Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(session replay): targeted replay capture #750

Draft
wants to merge 3 commits into
base: v1.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions packages/plugin-session-replay-browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [1.7.0-srtargeting.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-23)

### Bug Fixes

- **session replay:** update version
([1bdcde0](https://github.com/amplitude/Amplitude-TypeScript/commit/1bdcde094b45151a720c2d76ba47ad2129d8d043))

### Features

- **session replay:** add ability to capture replays based on targeting via remote config
([3daf644](https://github.com/amplitude/Amplitude-TypeScript/commit/3daf644e3fa471d88c5083375d745ace52b9f4f0))

# Change Log

All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.6.22](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-23)

**Note:** Version bump only for package @amplitude/plugin-session-replay-browser
Expand Down Expand Up @@ -41,16 +58,21 @@ All notable changes to this project will be documented in this file. See

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

**Note:** Version bump only for package @amplitude/plugin-session-replay-browser

# Change Log
### Bug Fixes

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

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

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

# Change Log

Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-session-replay-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@amplitude/plugin-session-replay-browser",
"version": "1.6.22",
"version": "1.7.0-srtargeting.2",
"description": "",
"author": "Amplitude Inc",
"homepage": "https://github.com/amplitude/Amplitude-TypeScript",
Expand Down Expand Up @@ -41,7 +41,7 @@
"@amplitude/analytics-client-common": ">=1 <3",
"@amplitude/analytics-core": ">=1 <3",
"@amplitude/analytics-types": ">=1 <3",
"@amplitude/session-replay-browser": "^1.13.6",
"@amplitude/session-replay-browser": "^1.14.0-srtargeting.2",
"idb-keyval": "^6.2.1",
"tslib": "^2.4.1"
},
Expand Down
11 changes: 11 additions & 0 deletions packages/plugin-session-replay-browser/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IdentifyOperation } from '@amplitude/analytics-types';

export const PROPERTY_ADD_OPERATIONS = [
IdentifyOperation.SET,
IdentifyOperation.SET_ONCE,
IdentifyOperation.ADD,
IdentifyOperation.APPEND,
IdentifyOperation.PREPEND,
IdentifyOperation.POSTINSERT,
IdentifyOperation.POSTINSERT,
];
22 changes: 22 additions & 0 deletions packages/plugin-session-replay-browser/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Event, IdentifyOperation } from '@amplitude/analytics-types';
import { PROPERTY_ADD_OPERATIONS } from './constants';

export const parseUserProperties = (event: Event) => {
if (!event.user_properties) {
return;
}
let userPropertiesObj = {};
const userPropertyKeys = Object.keys(event.user_properties);

userPropertyKeys.forEach((identifyKey) => {
if (PROPERTY_ADD_OPERATIONS.includes(identifyKey as IdentifyOperation)) {
const typedUserPropertiesOperation =
event.user_properties && (event.user_properties[identifyKey as IdentifyOperation] as Record<string, any>);
userPropertiesObj = {
...userPropertiesObj,
...typedUserPropertiesOperation,
};
}
});
return userPropertiesObj;
};
19 changes: 17 additions & 2 deletions packages/plugin-session-replay-browser/src/session-replay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types';
import { getAnalyticsConnector } from '@amplitude/analytics-client-common';
import { BrowserConfig, EnrichmentPlugin, Event, SpecialEventType } from '@amplitude/analytics-types';
import * as sessionReplay from '@amplitude/session-replay-browser';
import { parseUserProperties } from './helpers';
import { SessionReplayOptions } from './typings/session-replay';
import { VERSION } from './version';

Expand Down Expand Up @@ -43,6 +45,9 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
}
}

const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore;
const userProperties = identityStore.getIdentity().userProperties;

await sessionReplay.init(config.apiKey, {
instanceName: this.config.instanceName,
deviceId: this.config.deviceId,
Expand All @@ -63,6 +68,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
configEndpointUrl: this.options.configEndpointUrl,
shouldInlineStylesheet: this.options.shouldInlineStylesheet,
version: { type: 'plugin', version: VERSION },
userProperties: userProperties,
}).promise;
}

Expand All @@ -71,11 +77,20 @@ export class SessionReplayPlugin implements EnrichmentPlugin {
// Choosing not to read from event object here, concerned about offline/delayed events messing up the state stored
// in SR.
if (this.config.sessionId && this.config.sessionId !== sessionReplay.getSessionId()) {
await sessionReplay.setSessionId(this.config.sessionId).promise;
const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore;
const userProperties = identityStore.getIdentity().userProperties;
await sessionReplay.setSessionId(this.config.sessionId, this.config.deviceId, {
userProperties: userProperties || {},
}).promise;
}
// Treating config.sessionId as source of truth, if the event's session id doesn't match, the
// event is not of the current session (offline/late events). In that case, don't tag the events
if (this.config.sessionId && this.config.sessionId === event.session_id) {
let userProperties;
if (event.event_type === SpecialEventType.IDENTIFY) {
userProperties = parseUserProperties(event);
}
await sessionReplay.evaluateTargetingAndCapture({ event, userProperties });
const sessionRecordingProperties = sessionReplay.getSessionReplayProperties();
event.event_properties = {
...event.event_properties,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-session-replay-browser/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Autogenerated by `yarn version-file`. DO NOT EDIT
export const VERSION = '1.6.22';
export const VERSION = '1.7.0-srtargeting.2';
40 changes: 40 additions & 0 deletions packages/plugin-session-replay-browser/test/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SpecialEventType } from '@amplitude/analytics-types';
import { parseUserProperties } from '../src/helpers';

describe('helpers', () => {
test('should return undefined if no user properties', () => {
const userProperties = parseUserProperties({
event_type: SpecialEventType.IDENTIFY,
session_id: 123,
});
expect(userProperties).toEqual(undefined);
});

test('should parse properties from their operation', () => {
const userProperties = parseUserProperties({
event_type: SpecialEventType.IDENTIFY,
user_properties: {
$set: {
plan_id: 'free',
},
},
session_id: 123,
});
expect(userProperties).toEqual({
plan_id: 'free',
});
});

test('should return an empty object if operations are not additive', () => {
const userProperties = parseUserProperties({
event_type: SpecialEventType.IDENTIFY,
user_properties: {
$remove: {
plan_id: 'free',
},
},
session_id: 123,
});
expect(userProperties).toEqual({});
});
});
129 changes: 126 additions & 3 deletions packages/plugin-session-replay-browser/test/session-replay.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin } from '@amplitude/analytics-types';
import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin, SpecialEventType } from '@amplitude/analytics-types';
import * as sessionReplayBrowser from '@amplitude/session-replay-browser';
import { SessionReplayPlugin, sessionReplayPlugin } from '../src/session-replay';
import { VERSION } from '../src/version';
import * as AnalyticsClientCommon from '@amplitude/analytics-client-common';

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

describe('SessionReplayPlugin', () => {
const { init, setSessionId, getSessionReplayProperties, shutdown, getSessionId } =
const { init, setSessionId, getSessionReplayProperties, evaluateTargetingAndCapture, shutdown, getSessionId } =
sessionReplayBrowser as MockedSessionReplayBrowser;
const mockLoggerProvider: MockedLogger = {
error: jest.fn(),
Expand Down Expand Up @@ -93,6 +94,30 @@ describe('SessionReplayPlugin', () => {
expect(sessionReplay.config.flushIntervalMillis).toBe(0);
});

test('should pass user properties to plugin', async () => {
const updatedConfig: BrowserConfig = { ...mockConfig, sessionId: 456, instanceName: 'browser-sdk' };

const mockUserProperties = {
plan_id: 'free',
};
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
identityStore: {
getIdentity: () => {
return {
userProperties: mockUserProperties,
};
},
},
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
const sessionReplay = new SessionReplayPlugin();
await sessionReplay.setup(updatedConfig);
expect(init).toHaveBeenCalledTimes(1);
expect(init).toHaveBeenCalledWith(
mockConfig.apiKey,
expect.objectContaining({ userProperties: mockUserProperties }),
);
});

describe('defaultTracking', () => {
test('should not change defaultTracking if its set to true', async () => {
const sessionReplay = new SessionReplayPlugin();
Expand Down Expand Up @@ -182,6 +207,7 @@ describe('SessionReplayPlugin', () => {
type: 'plugin',
version: VERSION,
},
userProperties: {},
});
});
});
Expand Down Expand Up @@ -210,6 +236,55 @@ describe('SessionReplayPlugin', () => {
});
});

test('should evaluate targeting, passing the event', async () => {
const sessionReplay = sessionReplayPlugin();
await sessionReplay.setup(mockConfig, mockAmplitude);
getSessionReplayProperties.mockReturnValueOnce({
'[Amplitude] Session Replay ID': '123',
});
const event = {
event_type: 'event_type',
event_properties: {
property_a: true,
property_b: 123,
},
session_id: 123,
};

await sessionReplay.execute(event);

expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
event: event,
userProperties: undefined,
});
});

test('should parse user properties for identify event', async () => {
const sessionReplay = sessionReplayPlugin();
await sessionReplay.setup(mockConfig, mockAmplitude);
getSessionReplayProperties.mockReturnValueOnce({
'[Amplitude] Session Replay ID': '123',
});
const event = {
event_type: SpecialEventType.IDENTIFY,
user_properties: {
$set: {
plan_id: 'free',
},
},
session_id: 123,
};

await sessionReplay.execute(event);

expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({
event: event,
userProperties: {
plan_id: 'free',
},
});
});

test('should not add event property for for event with mismatching session id.', async () => {
const sessionReplay = sessionReplayPlugin();
await sessionReplay.setup({ ...mockConfig });
Expand Down Expand Up @@ -242,7 +317,55 @@ describe('SessionReplayPlugin', () => {
sessionReplay.config.sessionId = 456;
await sessionReplay.execute(newEvent);
expect(setSessionId).toHaveBeenCalledTimes(1);
expect(setSessionId).toHaveBeenCalledWith(456);
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} });
});

test('should update the session id on any event and pass along user properties', async () => {
const sessionReplay = new SessionReplayPlugin();
await sessionReplay.setup(mockConfig);

const event = {
event_type: 'session_start',
session_id: 456,
};
const mockUserProperties = {
plan_id: 'free',
};
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
identityStore: {
getIdentity: () => {
return {
userProperties: mockUserProperties,
};
},
},
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
sessionReplay.config.sessionId = 456;
await sessionReplay.execute(event);
expect(setSessionId).toHaveBeenCalledTimes(1);
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: mockUserProperties });
});
test('should update the session id on any event and pass along empty obj for user properties', async () => {
const sessionReplay = new SessionReplayPlugin();
await sessionReplay.setup(mockConfig);

const event = {
event_type: 'session_start',
session_id: 456,
};
jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({
identityStore: {
getIdentity: () => {
return {
userProperties: undefined,
};
},
},
} as unknown as ReturnType<typeof AnalyticsClientCommon.getAnalyticsConnector>);
sessionReplay.config.sessionId = 456;
await sessionReplay.execute(event);
expect(setSessionId).toHaveBeenCalledTimes(1);
expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} });
});

test('should not update if session id unchanged', async () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/session-replay-browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [1.14.0-srtargeting.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-23)

### Bug Fixes

- **session replay:** update version
([1bdcde0](https://github.com/amplitude/Amplitude-TypeScript/commit/1bdcde094b45151a720c2d76ba47ad2129d8d043))

### Features

- **session replay:** add ability to capture replays based on targeting via remote config
([3daf644](https://github.com/amplitude/Amplitude-TypeScript/commit/3daf644e3fa471d88c5083375d745ace52b9f4f0))

# Change Log

All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.13.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/[email protected]...@amplitude/[email protected]) (2024-08-23)

**Note:** Version bump only for package @amplitude/session-replay-browser
Expand Down
Loading