Skip to content

Commit 272189c

Browse files
author
Kelly Wallach
committed
feat(session replay): add ability to capture replays based on targeting via remote config
1 parent 19bcecd commit 272189c

25 files changed

+1284
-701
lines changed

packages/plugin-session-replay-browser/CHANGELOG.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,21 @@ All notable changes to this project will be documented in this file. See
2323

2424
## [1.6.18](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-13)
2525

26-
**Note:** Version bump only for package @amplitude/plugin-session-replay-browser
27-
28-
# Change Log
26+
### Bug Fixes
2927

30-
All notable changes to this project will be documented in this file. See
31-
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
28+
- **session replay plugin:** remove unused if check
29+
([fc63646](https://github.com/amplitude/Amplitude-TypeScript/commit/fc6364683c416778b0609f20370454ee45437230))
30+
- **session replay:** rebase fixes
31+
([8386ede](https://github.com/amplitude/Amplitude-TypeScript/commit/8386ede0f8f61b71ed0b73d78967d465ed3567e9))
3232

33-
## [1.6.17](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-12)
33+
### Features
3434

35-
**Note:** Version bump only for package @amplitude/plugin-session-replay-browser
35+
- **session replay plugin:** support targeting by user properties
36+
([ba8e27d](https://github.com/amplitude/Amplitude-TypeScript/commit/ba8e27d070b2015afc846f7ef02b745cff485d76))
37+
- **session replay:** add ability to target by single event trigger
38+
([dab74f7](https://github.com/amplitude/Amplitude-TypeScript/commit/dab74f73dd5946ac99e517c57d97819acf3677f4))
39+
- **session replay:** update method names, and ensure targeting is independent from sampling
40+
([6d3a7be](https://github.com/amplitude/Amplitude-TypeScript/commit/6d3a7be4169e6e0e3bd8c7103895374daea107bd))
3641

3742
# Change Log
3843

packages/plugin-session-replay-browser/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@amplitude/plugin-session-replay-browser",
3-
"version": "1.6.20",
3+
"version": "1.7.0-srtargeting.0",
44
"description": "",
55
"author": "Amplitude Inc",
66
"homepage": "https://github.com/amplitude/Amplitude-TypeScript",
@@ -41,7 +41,7 @@
4141
"@amplitude/analytics-client-common": ">=1 <3",
4242
"@amplitude/analytics-core": ">=1 <3",
4343
"@amplitude/analytics-types": ">=1 <3",
44-
"@amplitude/session-replay-browser": "^1.13.4",
44+
"@amplitude/session-replay-browser": "^1.14.0-srtargeting.0",
4545
"idb-keyval": "^6.2.1",
4646
"tslib": "^2.4.1"
4747
},
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { IdentifyOperation } from '@amplitude/analytics-types';
2+
3+
export const PROPERTY_ADD_OPERATIONS = [
4+
IdentifyOperation.SET,
5+
IdentifyOperation.SET_ONCE,
6+
IdentifyOperation.ADD,
7+
IdentifyOperation.APPEND,
8+
IdentifyOperation.PREPEND,
9+
IdentifyOperation.POSTINSERT,
10+
IdentifyOperation.POSTINSERT,
11+
];
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Event, IdentifyOperation } from '@amplitude/analytics-types';
2+
import { PROPERTY_ADD_OPERATIONS } from './constants';
3+
4+
export const parseUserProperties = (event: Event) => {
5+
if (!event.user_properties) {
6+
return;
7+
}
8+
let userPropertiesObj = {};
9+
const userPropertyKeys = Object.keys(event.user_properties);
10+
11+
userPropertyKeys.forEach((identifyKey) => {
12+
if (PROPERTY_ADD_OPERATIONS.includes(identifyKey as IdentifyOperation)) {
13+
const typedUserPropertiesOperation =
14+
event.user_properties && (event.user_properties[identifyKey as IdentifyOperation] as Record<string, any>);
15+
userPropertiesObj = {
16+
...userPropertiesObj,
17+
...typedUserPropertiesOperation,
18+
};
19+
}
20+
});
21+
return userPropertiesObj;
22+
};

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types';
1+
import { getAnalyticsConnector } from '@amplitude/analytics-client-common';
2+
import { BrowserConfig, EnrichmentPlugin, Event, SpecialEventType } from '@amplitude/analytics-types';
23
import * as sessionReplay from '@amplitude/session-replay-browser';
4+
import { parseUserProperties } from './helpers';
35
import { SessionReplayOptions } from './typings/session-replay';
46
import { VERSION } from './version';
57

@@ -43,6 +45,9 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
4345
}
4446
}
4547

48+
const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore;
49+
const userProperties = identityStore.getIdentity().userProperties;
50+
4651
await sessionReplay.init(config.apiKey, {
4752
instanceName: this.config.instanceName,
4853
deviceId: this.config.deviceId,
@@ -63,6 +68,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
6368
configEndpointUrl: this.options.configEndpointUrl,
6469
shouldInlineStylesheet: this.options.shouldInlineStylesheet,
6570
version: { type: 'plugin', version: VERSION },
71+
userProperties: userProperties,
6672
}).promise;
6773
}
6874

@@ -71,11 +77,20 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
7177
// Choosing not to read from event object here, concerned about offline/delayed events messing up the state stored
7278
// in SR.
7379
if (this.config.sessionId && this.config.sessionId !== sessionReplay.getSessionId()) {
74-
await sessionReplay.setSessionId(this.config.sessionId).promise;
80+
const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore;
81+
const userProperties = identityStore.getIdentity().userProperties;
82+
await sessionReplay.setSessionId(this.config.sessionId, this.config.deviceId, {
83+
userProperties: userProperties || {},
84+
}).promise;
7585
}
7686
// Treating config.sessionId as source of truth, if the event's session id doesn't match, the
7787
// event is not of the current session (offline/late events). In that case, don't tag the events
7888
if (this.config.sessionId && this.config.sessionId === event.session_id) {
89+
let userProperties;
90+
if (event.event_type === SpecialEventType.IDENTIFY) {
91+
userProperties = parseUserProperties(event);
92+
}
93+
await sessionReplay.evaluateTargetingAndCapture({ event, userProperties });
7994
const sessionRecordingProperties = sessionReplay.getSessionReplayProperties();
8095
event.event_properties = {
8196
...event.event_properties,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// Autogenerated by `yarn version-file`. DO NOT EDIT
2-
export const VERSION = '1.6.20';
2+
export const VERSION = '1.7.0-srtargeting.0';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { SpecialEventType } from '@amplitude/analytics-types';
2+
import { parseUserProperties } from '../src/helpers';
3+
4+
describe('helpers', () => {
5+
test('should return undefined if no user properties', () => {
6+
const userProperties = parseUserProperties({
7+
event_type: SpecialEventType.IDENTIFY,
8+
session_id: 123,
9+
});
10+
expect(userProperties).toEqual(undefined);
11+
});
12+
13+
test('should parse properties from their operation', () => {
14+
const userProperties = parseUserProperties({
15+
event_type: SpecialEventType.IDENTIFY,
16+
user_properties: {
17+
$set: {
18+
plan_id: 'free',
19+
},
20+
},
21+
session_id: 123,
22+
});
23+
expect(userProperties).toEqual({
24+
plan_id: 'free',
25+
});
26+
});
27+
28+
test('should return an empty object if operations are not additive', () => {
29+
const userProperties = parseUserProperties({
30+
event_type: SpecialEventType.IDENTIFY,
31+
user_properties: {
32+
$remove: {
33+
plan_id: 'free',
34+
},
35+
},
36+
session_id: 123,
37+
});
38+
expect(userProperties).toEqual({});
39+
});
40+
});

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

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin } from '@amplitude/analytics-types';
1+
import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin, SpecialEventType } from '@amplitude/analytics-types';
22
import * as sessionReplayBrowser from '@amplitude/session-replay-browser';
33
import { SessionReplayPlugin, sessionReplayPlugin } from '../src/session-replay';
44
import { VERSION } from '../src/version';
5+
import * as AnalyticsClientCommon from '@amplitude/analytics-client-common';
56

67
jest.mock('@amplitude/session-replay-browser');
78
type MockedSessionReplayBrowser = jest.Mocked<typeof import('@amplitude/session-replay-browser')>;
@@ -11,7 +12,7 @@ type MockedLogger = jest.Mocked<Logger>;
1112
type MockedBrowserClient = jest.Mocked<BrowserClient>;
1213

1314
describe('SessionReplayPlugin', () => {
14-
const { init, setSessionId, getSessionReplayProperties, shutdown, getSessionId } =
15+
const { init, setSessionId, getSessionReplayProperties, evaluateTargetingAndCapture, shutdown, getSessionId } =
1516
sessionReplayBrowser as MockedSessionReplayBrowser;
1617
const mockLoggerProvider: MockedLogger = {
1718
error: jest.fn(),
@@ -93,6 +94,30 @@ describe('SessionReplayPlugin', () => {
9394
expect(sessionReplay.config.flushIntervalMillis).toBe(0);
9495
});
9596

97+
test('should pass user properties to plugin', async () => {
98+
const updatedConfig: BrowserConfig = { ...mockConfig, sessionId: 456, instanceName: 'browser-sdk' };
99+
100+
const mockUserProperties = {
101+
plan_id: 'free',
102+
};
103+
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
104+
identityStore: {
105+
getIdentity: () => {
106+
return {
107+
userProperties: mockUserProperties,
108+
};
109+
},
110+
},
111+
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
112+
const sessionReplay = new SessionReplayPlugin();
113+
await sessionReplay.setup(updatedConfig);
114+
expect(init).toHaveBeenCalledTimes(1);
115+
expect(init).toHaveBeenCalledWith(
116+
mockConfig.apiKey,
117+
expect.objectContaining({ userProperties: mockUserProperties }),
118+
);
119+
});
120+
96121
describe('defaultTracking', () => {
97122
test('should not change defaultTracking if its set to true', async () => {
98123
const sessionReplay = new SessionReplayPlugin();
@@ -182,6 +207,7 @@ describe('SessionReplayPlugin', () => {
182207
type: 'plugin',
183208
version: VERSION,
184209
},
210+
userProperties: {},
185211
});
186212
});
187213
});
@@ -210,6 +236,55 @@ describe('SessionReplayPlugin', () => {
210236
});
211237
});
212238

239+
test('should evaluate targeting, passing the event', async () => {
240+
const sessionReplay = sessionReplayPlugin();
241+
await sessionReplay.setup(mockConfig, mockAmplitude);
242+
getSessionReplayProperties.mockReturnValueOnce({
243+
'[Amplitude] Session Replay ID': '123',
244+
});
245+
const event = {
246+
event_type: 'event_type',
247+
event_properties: {
248+
property_a: true,
249+
property_b: 123,
250+
},
251+
session_id: 123,
252+
};
253+
254+
await sessionReplay.execute(event);
255+
256+
expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
257+
event: event,
258+
userProperties: undefined,
259+
});
260+
});
261+
262+
test('should parse user properties for identify event', async () => {
263+
const sessionReplay = sessionReplayPlugin();
264+
await sessionReplay.setup(mockConfig, mockAmplitude);
265+
getSessionReplayProperties.mockReturnValueOnce({
266+
'[Amplitude] Session Replay ID': '123',
267+
});
268+
const event = {
269+
event_type: SpecialEventType.IDENTIFY,
270+
user_properties: {
271+
$set: {
272+
plan_id: 'free',
273+
},
274+
},
275+
session_id: 123,
276+
};
277+
278+
await sessionReplay.execute(event);
279+
280+
expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
281+
event: event,
282+
userProperties: {
283+
plan_id: 'free',
284+
},
285+
});
286+
});
287+
213288
test('should not add event property for for event with mismatching session id.', async () => {
214289
const sessionReplay = sessionReplayPlugin();
215290
await sessionReplay.setup({ ...mockConfig });
@@ -242,7 +317,55 @@ describe('SessionReplayPlugin', () => {
242317
sessionReplay.config.sessionId = 456;
243318
await sessionReplay.execute(newEvent);
244319
expect(setSessionId).toHaveBeenCalledTimes(1);
245-
expect(setSessionId).toHaveBeenCalledWith(456);
320+
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} });
321+
});
322+
323+
test('should update the session id on any event and pass along user properties', async () => {
324+
const sessionReplay = new SessionReplayPlugin();
325+
await sessionReplay.setup(mockConfig);
326+
327+
const event = {
328+
event_type: 'session_start',
329+
session_id: 456,
330+
};
331+
const mockUserProperties = {
332+
plan_id: 'free',
333+
};
334+
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
335+
identityStore: {
336+
getIdentity: () => {
337+
return {
338+
userProperties: mockUserProperties,
339+
};
340+
},
341+
},
342+
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
343+
sessionReplay.config.sessionId = 456;
344+
await sessionReplay.execute(event);
345+
expect(setSessionId).toHaveBeenCalledTimes(1);
346+
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: mockUserProperties });
347+
});
348+
test('should update the session id on any event and pass along empty obj for user properties', async () => {
349+
const sessionReplay = new SessionReplayPlugin();
350+
await sessionReplay.setup(mockConfig);
351+
352+
const event = {
353+
event_type: 'session_start',
354+
session_id: 456,
355+
};
356+
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
357+
identityStore: {
358+
getIdentity: () => {
359+
return {
360+
userProperties: undefined,
361+
};
362+
},
363+
},
364+
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
365+
sessionReplay.config.sessionId = 456;
366+
await sessionReplay.execute(event);
367+
expect(setSessionId).toHaveBeenCalledTimes(1);
368+
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} });
246369
});
247370

248371
test('should not update if session id unchanged', async () => {

packages/session-replay-browser/README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@ sessionReplay.init(API_KEY, {
4747
});
4848
```
4949

50-
### 3. Get session replay event properties
51-
Any event that occurs within the span of a session replay must be tagged with properties that signal to Amplitude to include it in the scope of the replay. The following shows an example of how to use the properties
50+
### 3. Evaluate targeting (optional)
51+
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.
52+
```typescript
53+
const sessionTargetingMatch = sessionReplay.evaluateTargetingAndCapture({ event: {
54+
event_type: EVENT_NAME,
55+
time: EVENT_TIMESTAMP,
56+
event_properties: eventProperties
57+
} });
58+
```
59+
60+
### 4. Get session replay event properties
61+
Any event must be tagged with properties that signal to Amplitude to include it in the scope of the replay. The following shows an example of how to use the properties.
5262
```typescript
5363
const sessionReplayProperties = sessionReplay.getSessionReplayProperties();
5464
track(EVENT_NAME, {
@@ -57,7 +67,7 @@ track(EVENT_NAME, {
5767
})
5868
```
5969

60-
### 4. Update session id
70+
### 5. Update session id
6171
Any time that the session id for the user changes, the session replay SDK must be notified of that change. Update the session id via the following method:
6272
```typescript
6373
sessionReplay.setSessionId(UNIX_TIMESTAMP)
@@ -67,7 +77,7 @@ You can optionally pass a new device id as a second argument as well:
6777
sessionReplay.setSessionId(UNIX_TIMESTAMP, deviceId)
6878
```
6979

70-
### 5. Shutdown (optional)
80+
### 6. Shutdown (optional)
7181
If at any point you would like to discontinue collection of session replays, for example in a part of your application where you would not like sessions to be collected, you can use the following method to stop collection and remove collection event listeners.
7282
```typescript
7383
sessionReplay.shutdown()

0 commit comments

Comments
 (0)