Skip to content

Commit a18bc3e

Browse files
author
Kelly Wallach
committed
feat(session replay): update method names, and ensure targeting is independent from sampling
1 parent 1fb346c commit a18bc3e

File tree

13 files changed

+260
-592
lines changed

13 files changed

+260
-592
lines changed

packages/plugin-session-replay-browser/src/session-replay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class SessionReplayEnrichmentPlugin implements EnrichmentPlugin {
4848
if (event.event_type === SpecialEventType.IDENTIFY) {
4949
userProperties = parseUserProperties(event);
5050
}
51-
await sessionReplay.evaluateTargetingAndRecord({ event, userProperties });
51+
await sessionReplay.evaluateTargetingAndCapture({ event, userProperties });
5252
const sessionRecordingProperties = sessionReplay.getSessionReplayProperties();
5353
event.event_properties = {
5454
...event.event_properties,

packages/plugin-session-replay-browser/test/session-replay.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type MockedLogger = jest.Mocked<Logger>;
2020
type MockedBrowserClient = jest.Mocked<BrowserClient>;
2121

2222
describe('SessionReplayPlugin', () => {
23-
const { init, setSessionId, getSessionReplayProperties, evaluateTargetingAndRecord, flush, shutdown, getSessionId } =
23+
const { init, setSessionId, getSessionReplayProperties, evaluateTargetingAndCapture, flush, shutdown, getSessionId } =
2424
sessionReplayBrowser as MockedSessionReplayBrowser;
2525
const mockLoggerProvider: MockedLogger = {
2626
error: jest.fn(),
@@ -334,7 +334,7 @@ describe('SessionReplayPlugin', () => {
334334
await sessionReplayEnrichmentPlugin.setup(mockConfig);
335335
await sessionReplayEnrichmentPlugin.execute(event);
336336

337-
expect(evaluateTargetingAndRecord).toHaveBeenCalledWith({
337+
expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
338338
event: event,
339339
userProperties: undefined,
340340
});
@@ -360,7 +360,7 @@ describe('SessionReplayPlugin', () => {
360360
await sessionReplayEnrichmentPlugin.setup(mockConfig);
361361
await sessionReplayEnrichmentPlugin.execute(event);
362362

363-
expect(evaluateTargetingAndRecord).toHaveBeenCalledWith({
363+
expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
364364
event: event,
365365
userProperties: {
366366
plan_id: 'free',

packages/session-replay-browser/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ sessionReplay.init(API_KEY, {
5050
### 3. Evaluate targeting (optional)
5151
Any event that occurs within the span of a session replay must be passed to the SDK to evaluate against targeting conditions. This should be done *before* step 4, getting the event properties. If you are not using the targeting condition logic provided via the Amplitude UI, this step is not required.
5252
```typescript
53-
const sessionTargetingMatch = sessionReplay.evaluateTargetingAndRecord({ event: {
53+
const sessionTargetingMatch = sessionReplay.evaluateTargetingAndCapture({ event: {
5454
event_type: EVENT_NAME,
5555
time: EVENT_TIMESTAMP,
5656
event_properties: eventProperties

packages/session-replay-browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@amplitude/analytics-remote-config": "^0.3.4",
4444
"@amplitude/analytics-types": ">=1 <3",
4545
"@amplitude/rrweb": "2.0.0-alpha.19",
46-
"@amplitude/targeting": "0.1.1",
46+
"@amplitude/targeting": "0.2.0",
4747
"idb": "^8.0.0",
4848
"tslib": "^2.4.1"
4949
},

packages/session-replay-browser/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const {
33
init,
44
setSessionId,
55
getSessionId,
6-
evaluateTargetingAndRecord,
6+
evaluateTargetingAndCapture,
77
getSessionReplayProperties,
88
flush,
99
shutdown,

packages/session-replay-browser/src/session-replay-factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const createInstance: () => AmplitudeSessionReplay = () => {
1717
const sessionReplay = new SessionReplay();
1818
return {
1919
init: debugWrapper(sessionReplay.init.bind(sessionReplay), 'init', getLogConfig(sessionReplay)),
20-
evaluateTargetingAndRecord: debugWrapper(
21-
sessionReplay.evaluateTargetingAndRecord.bind(sessionReplay),
20+
evaluateTargetingAndCapture: debugWrapper(
21+
sessionReplay.evaluateTargetingAndCapture.bind(sessionReplay),
2222
'evaluateTargetingAndRecord',
2323
getLogConfig(sessionReplay),
2424
),

packages/session-replay-browser/src/session-replay.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class SessionReplay implements AmplitudeSessionReplay {
174174
return {};
175175
}
176176

177-
const shouldRecord = this.getShouldRecord();
177+
const shouldRecord = this.getShouldCapture();
178178

179179
if (shouldRecord) {
180180
const eventProperties: { [key: string]: string | null } = {
@@ -261,9 +261,9 @@ export class SessionReplay implements AmplitudeSessionReplay {
261261
return identityStoreOptOut !== undefined ? identityStoreOptOut : this.config?.optOut;
262262
}
263263

264-
getShouldRecord() {
264+
getShouldCapture() {
265265
if (!this.identifiers || !this.config || !this.identifiers.sessionId) {
266-
this.loggerProvider.warn(`Session is not being recorded due to lack of config, please call sessionReplay.init.`);
266+
this.loggerProvider.warn(`Session is not being captured due to lack of config, please call sessionReplay.init.`);
267267
return false;
268268
}
269269
if (!this.config.captureEnabled) {
@@ -274,7 +274,9 @@ export class SessionReplay implements AmplitudeSessionReplay {
274274
}
275275

276276
if (this.shouldOptOut()) {
277-
this.loggerProvider.log(`Opting session ${this.identifiers.sessionId} out of recording due to optOut config.`);
277+
this.loggerProvider.log(
278+
`Opting session ${this.identifiers.sessionId} out of replay capture due to optOut config.`,
279+
);
278280
return false;
279281
}
280282

@@ -287,7 +289,6 @@ export class SessionReplay implements AmplitudeSessionReplay {
287289
);
288290
return false;
289291
} else {
290-
// TODO: is this log too noisy?
291292
this.loggerProvider.log(
292293
`Capturing replays for session ${this.identifiers.sessionId} due to matching targeting conditions.`,
293294
);
@@ -334,12 +335,11 @@ export class SessionReplay implements AmplitudeSessionReplay {
334335
}
335336

336337
captureEventsIfShould() {
337-
const shouldRecord = this.getShouldRecord();
338+
const shouldRecord = this.getShouldCapture();
338339
const sessionId = this.identifiers?.sessionId;
339340
if (!shouldRecord || !sessionId || !this.config) {
340341
return;
341342
}
342-
343343
if (this.recordCancelCallback) {
344344
this.loggerProvider.debug('captureEvents method fired - Session Replay capture already in progress.');
345345
return;

packages/session-replay-browser/src/targeting/targeting-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export const evaluateTargetingAndStore = async ({
4343
apiKey: apiKey,
4444
loggerProvider: loggerProvider,
4545
});
46-
47-
sessionTargetingMatch = targetingResult.sr_targeting_config.key === 'on';
46+
if (targetingResult && targetingResult.sr_targeting_config) {
47+
sessionTargetingMatch = targetingResult.sr_targeting_config.key === 'on';
48+
}
4849

4950
void targetingIDBStore.storeTargetingMatchForSession({
5051
loggerProvider: loggerProvider,

packages/session-replay-browser/test/flag-config-data.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ export const flagConfig = {
99
metadata: { segmentName: 'sign in trigger' },
1010
bucket: {
1111
selector: ['context', 'session_id'],
12-
salt: 'xdfrewd', // Different salt for each bucket to allow for fallthrough
12+
salt: 'xdfrewd',
1313
allocations: [
1414
{
15-
range: [0, 19], // Selects 20% of users that match these conditions
15+
range: [0, 99],
1616
distributions: [
1717
{
1818
variant: 'on',
@@ -36,10 +36,10 @@ export const flagConfig = {
3636
metadata: { segmentName: 'user property' },
3737
bucket: {
3838
selector: ['context', 'session_id'],
39-
salt: 'Rpr5h4vy', // Different salt for each bucket to allow for fallthrough
39+
salt: 'Rpr5h4vy',
4040
allocations: [
4141
{
42-
range: [0, 14], // Selects 15% of users that match these conditions
42+
range: [0, 99],
4343
distributions: [
4444
{
4545
variant: 'on',
@@ -63,7 +63,7 @@ export const flagConfig = {
6363
metadata: { segmentName: 'leftover allocation' },
6464
bucket: {
6565
selector: ['context', 'session_id'],
66-
salt: 'T5lhyRo', // Different salt for each bucket to allow for fallthrough
66+
salt: 'T5lhyRo',
6767
allocations: [
6868
{
6969
range: [0, 9], // Selects 10% of users that match these conditions

packages/session-replay-browser/test/integration/sampling.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_SE
1313
import * as Helpers from '../../src/helpers';
1414
import { SessionReplay } from '../../src/session-replay';
1515
import { SESSION_ID_IN_20_SAMPLE } from '../test-data';
16+
import { SessionReplayRemoteConfig } from '../../src/config/types';
17+
import { flagConfig } from '../flag-config-data';
1618

1719
type MockedLogger = jest.Mocked<Logger>;
1820
jest.mock('@amplitude/rrweb');
@@ -159,9 +161,14 @@ describe('module level integration', () => {
159161
expect(inSampleSpy).toHaveBeenCalledWith(sessionReplay.identifiers?.sessionId, 0.8);
160162
});
161163
});
162-
describe('with remote config set', () => {
164+
describe('with sampling config in remote config', () => {
163165
beforeEach(() => {
164-
getRemoteConfigMock.mockResolvedValue(samplingConfig);
166+
getRemoteConfigMock.mockImplementation((namespace: string, key: keyof SessionReplayRemoteConfig) => {
167+
if (namespace === 'sessionReplay' && key === 'sr_sampling_config') {
168+
return samplingConfig;
169+
}
170+
return;
171+
});
165172
});
166173
test('should capture', async () => {
167174
const sessionReplay = new SessionReplay();
@@ -197,6 +204,66 @@ describe('module level integration', () => {
197204
expect(inSampleSpy).toHaveBeenCalledWith(sessionReplay.identifiers?.sessionId, 0.5);
198205
});
199206
});
207+
describe('with sampling config and targeting config in remote config', () => {
208+
beforeEach(() => {
209+
getRemoteConfigMock.mockImplementation((namespace: string, key: keyof SessionReplayRemoteConfig) => {
210+
if (namespace === 'sessionReplay' && key === 'sr_sampling_config') {
211+
return samplingConfig;
212+
}
213+
if (namespace === 'sessionReplay' && key === 'sr_targeting_config') {
214+
return flagConfig;
215+
}
216+
return;
217+
});
218+
});
219+
test('should not capture if no targeting match', async () => {
220+
const sessionReplay = new SessionReplay();
221+
await sessionReplay.init(apiKey, { ...mockOptions }).promise;
222+
const sessionRecordingProperties = sessionReplay.getSessionReplayProperties();
223+
const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0]
224+
.value;
225+
226+
jest.spyOn(createEventsIDBStoreInstance, 'storeCurrentSequence');
227+
expect(sessionRecordingProperties).not.toMatchObject({
228+
[DEFAULT_SESSION_REPLAY_PROPERTY]: `1a2b3c/${SESSION_ID_IN_20_SAMPLE}`,
229+
});
230+
expect(record).not.toHaveBeenCalled();
231+
});
232+
test('should capture if targeting match', async () => {
233+
const sessionReplay = new SessionReplay();
234+
await sessionReplay.init(apiKey, { ...mockOptions }).promise;
235+
await sessionReplay.evaluateTargetingAndCapture({ event: { event_type: 'Sign In' } });
236+
const sessionRecordingProperties = sessionReplay.getSessionReplayProperties();
237+
const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0]
238+
.value;
239+
240+
jest.spyOn(createEventsIDBStoreInstance, 'storeCurrentSequence');
241+
expect(sessionRecordingProperties).toMatchObject({
242+
[DEFAULT_SESSION_REPLAY_PROPERTY]: `1a2b3c/${SESSION_ID_IN_20_SAMPLE}`,
243+
});
244+
expect(record).toHaveBeenCalled();
245+
const recordArg = record.mock.calls[0][0];
246+
recordArg?.emit && recordArg?.emit(mockEvent);
247+
sessionReplay.sendEvents();
248+
await (createEventsIDBStoreInstance.storeCurrentSequence as jest.Mock).mock.results[0].value;
249+
await runScheduleTimers();
250+
expect(fetch).toHaveBeenLastCalledWith(
251+
`${SESSION_REPLAY_SERVER_URL}?device_id=1a2b3c&session_id=${SESSION_ID_IN_20_SAMPLE}&seq_number=1&type=replay`,
252+
expect.anything(),
253+
);
254+
// eslint-disable-next-line @typescript-eslint/unbound-method
255+
expect(mockLoggerProvider.log).toHaveBeenLastCalledWith(
256+
'Session replay event batch with seq id 1 tracked successfully for session id 1719847315000, size of events: 0 KB',
257+
);
258+
});
259+
260+
test('should not use sampleRate', async () => {
261+
const inSampleSpy = jest.spyOn(Helpers, 'isSessionInSample');
262+
const sessionReplay = new SessionReplay();
263+
await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.8 }).promise;
264+
expect(inSampleSpy).not.toHaveBeenCalled();
265+
});
266+
});
200267
});
201268
describe('sampling logic', () => {
202269
beforeEach(() => {

0 commit comments

Comments
 (0)