Skip to content

Commit 27cbf45

Browse files
feat(segment-plugin-session-replay-react-native) add segment plugin for session replay (#1186)
1 parent 6894b93 commit 27cbf45

File tree

11 files changed

+578
-0
lines changed

11 files changed

+578
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# @amplitude/segment-session-replay-plugin-react-native
2+
3+
Amplitude Segment Session Replay Plugin for React Native
4+
5+
## Installation
6+
7+
```sh
8+
npm install @amplitude/segment-session-replay-plugin-react-native
9+
```
10+
11+
### Dependencies
12+
13+
This plugin requires the following dependencies to be installed:
14+
15+
```sh
16+
npm install @amplitude/session-replay-react-native @segment/analytics-react-native
17+
```
18+
19+
**Important**: This plugin requires the `@segment/analytics-react-native-plugin-amplitude-session` plugin to extract session IDs from Amplitude integration data. Make sure to add this plugin to your Segment client **before** adding the session replay plugin.
20+
21+
## Usage
22+
23+
### Segment Session Replay Plugin
24+
25+
Add the segment session replay plugin to your Segment Analytics instance to automatically integrate Amplitude Session Replay with your Segment events.
26+
27+
```js
28+
import { createSegmentSessionReplayPlugin } from '@amplitude/segment-session-replay-plugin-react-native';
29+
import { createClient } from '@segment/analytics-react-native';
30+
import { AmplitudeSessionPlugin } from '@segment/analytics-react-native-plugin-amplitude-session';
31+
32+
// Initialize Segment client
33+
const segmentClient = createClient({
34+
writeKey: 'YOUR_SEGMENT_WRITE_KEY',
35+
});
36+
37+
// Configure session replay plugin
38+
const sessionReplayConfig = {
39+
apiKey: 'YOUR_AMPLITUDE_API_KEY',
40+
deviceId: 'YOUR_DEVICE_ID'
41+
};
42+
43+
// Add the Amplitude session plugin first (required for session ID extraction)
44+
await segmentClient.add({ plugin: new AmplitudeSessionPlugin() });
45+
46+
// Add the session replay plugin to Segment
47+
await segmentClient.add(createSegmentSessionReplayPlugin(sessionReplayConfig));
48+
```
49+
50+
### Plugin Configuration
51+
52+
The plugin accepts a `SessionReplayConfig` object
53+
54+
### Automatic Integration
55+
56+
The plugin automatically:
57+
58+
1. **Initializes Session Replay**: Sets up Amplitude Session Replay with your configuration
59+
2. **Syncs Session Data**: Updates session ID and device ID for each Segment event
60+
3. **Enriches Events**: Adds session replay properties to track and screen events
61+
4. **Manages Lifecycle**: Handles start/stop operations for session replay
62+
63+
### Event Processing
64+
65+
The plugin processes the following Segment event types:
66+
- `TrackEvent`: Adds session replay properties to track events
67+
- `ScreenEvent`: Adds session replay properties to screen events
68+
69+
For these events, the plugin:
70+
- Extracts session ID from event properties or Amplitude integration data
71+
- Extracts device ID from event context or anonymous ID
72+
- Adds session replay properties to the event before sending to Segment
73+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: ['module:metro-react-native-babel-preset'],
3+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const baseConfig = require('../../jest.config.js');
2+
const package = require('./package');
3+
4+
module.exports = {
5+
...baseConfig,
6+
displayName: package.name,
7+
rootDir: '.',
8+
preset: 'react-native',
9+
testEnvironment: 'jsdom',
10+
modulePathIgnorePatterns: ['<rootDir>/lib/'],
11+
moduleFileExtensions: ['tsx', 'ts', 'js', 'jsx', 'json'],
12+
// TODO do 100% coverage
13+
coverageThreshold: {
14+
global: {
15+
branches: 80,
16+
functions: 80,
17+
lines: 80,
18+
statements: 80,
19+
},
20+
},
21+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Mock React Native modules that cause issues in Jest
2+
jest.mock('react-native', () => ({
3+
Platform: {
4+
OS: 'ios',
5+
select: jest.fn(),
6+
},
7+
NativeModules: {},
8+
NativeEventEmitter: jest.fn(),
9+
}));
10+
11+
// Mock @react-native/polyfills
12+
jest.mock('@react-native/polyfills', () => ({}));
13+
14+
// Mock any other React Native related modules that might cause issues
15+
jest.mock('@react-native-async-storage/async-storage', () => ({
16+
getItem: jest.fn(),
17+
setItem: jest.fn(),
18+
removeItem: jest.fn(),
19+
clear: jest.fn(),
20+
}));
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"name": "@amplitude/segment-session-replay-plugin-react-native",
3+
"version": "0.0.1-beta.1",
4+
"description": "Amplitude Segment Session Replay Plugin for React Native",
5+
"keywords": [
6+
"analytics",
7+
"amplitude",
8+
"react-native",
9+
"ios",
10+
"android",
11+
"session-replay"
12+
],
13+
"author": "Amplitude Inc",
14+
"homepage": "https://github.com/amplitude/Amplitude-TypeScript",
15+
"license": "MIT",
16+
"main": "lib/commonjs/index",
17+
"module": "lib/module/index",
18+
"types": "lib/typescript/index.d.ts",
19+
"react-native": "src/index.ts",
20+
"source": "src/index.ts",
21+
"publishConfig": {
22+
"access": "public",
23+
"tag": "latest"
24+
},
25+
"repository": {
26+
"type": "git",
27+
"url": "git+https://github.com/amplitude/Amplitude-TypeScript.git"
28+
},
29+
"scripts": {
30+
"build": "bob build",
31+
"fix": "yarn fix:eslint & yarn fix:prettier",
32+
"fix:eslint": "eslint '{src,test}/**/*.ts' --fix",
33+
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
34+
"lint": "yarn lint:prettier && yarn lint:eslint",
35+
"lint:eslint": "eslint '{src,test}/**/*.ts'",
36+
"lint:prettier": "prettier --check \"{src,test}/**/*.ts\"",
37+
"test": "jest",
38+
"typecheck": "tsc -p ./tsconfig.json",
39+
"version": "yarn version-file && yarn build",
40+
"version-file": "node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts",
41+
"typescript": "tsc --noEmit"
42+
},
43+
"files": [
44+
"src",
45+
"!test",
46+
"!**/__tests__",
47+
"!**/__fixtures__",
48+
"!**/__mocks__",
49+
"!**/.*"
50+
],
51+
"bugs": {
52+
"url": "https://github.com/amplitude/Amplitude-TypeScript/issues"
53+
},
54+
"devDependencies": {
55+
"@amplitude/session-replay-react-native": "0.0.1-beta.1",
56+
"@segment/analytics-react-native": "2.15.0",
57+
"react-native-builder-bob": "^0.20.3"
58+
},
59+
"peerDependencies": {
60+
"@amplitude/session-replay-react-native": "*",
61+
"@segment/analytics-react-native": ">= 2.2.0 < 3"
62+
},
63+
"react-native-builder-bob": {
64+
"source": "src",
65+
"output": "lib",
66+
"targets": [
67+
"commonjs",
68+
"module",
69+
[
70+
"typescript",
71+
{
72+
"project": "tsconfig.build.json"
73+
}
74+
]
75+
]
76+
}
77+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { createSegmentSessionReplayPlugin } from './segment-session-replay-plugin';
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Plugin, PluginType, type SegmentEvent, EventType, SegmentClient } from '@segment/analytics-react-native';
2+
3+
import {
4+
type SessionReplayConfig,
5+
getSessionReplayProperties,
6+
init,
7+
setDeviceId,
8+
setSessionId,
9+
start,
10+
stop,
11+
} from '@amplitude/session-replay-react-native';
12+
import { VERSION } from './version';
13+
14+
function getSessionId(event: SegmentEvent): number {
15+
const amplitudeSessionId =
16+
(event.integrations?.['Actions Amplitude'] as { session_id: number })?.['session_id'] ?? null;
17+
if (amplitudeSessionId !== null) {
18+
return amplitudeSessionId;
19+
}
20+
21+
if (event.type === EventType.TrackEvent || event.type === EventType.ScreenEvent) {
22+
const sessionIdRaw = event.properties?.['session_id'];
23+
const sessionId = Number(sessionIdRaw);
24+
return Number.isNaN(sessionId) ? -1 : sessionId;
25+
}
26+
return -1;
27+
}
28+
29+
function getDeviceId(event: SegmentEvent): string | null {
30+
return event.context?.device?.id ?? event.anonymousId ?? null;
31+
}
32+
33+
export class SegmentSessionReplayPlugin extends Plugin {
34+
name = 'amplitude-segment-session-replay-plugin-react-native';
35+
version: string = VERSION;
36+
type: PluginType = PluginType.enrichment;
37+
38+
private sessionReplayConfig: SessionReplayConfig;
39+
40+
// @review: This is to ensure the plugin is initialized before the first event is processed.
41+
// because `configure` is not asynchronous
42+
private initPromise: Promise<void> | null = null;
43+
44+
constructor(config: SessionReplayConfig) {
45+
super();
46+
this.sessionReplayConfig = config;
47+
}
48+
49+
async configure(analytics: SegmentClient): Promise<void> {
50+
super.configure(analytics);
51+
this.initPromise = init({
52+
deviceId: analytics.userInfo.get().anonymousId,
53+
...this.sessionReplayConfig,
54+
});
55+
await this.initPromise;
56+
}
57+
58+
async execute(event: SegmentEvent): Promise<SegmentEvent> {
59+
await this.initPromise;
60+
61+
const sessionId = getSessionId(event);
62+
const deviceId = getDeviceId(event);
63+
64+
await setSessionId(sessionId);
65+
await setDeviceId(deviceId);
66+
67+
if (event.type === EventType.TrackEvent || event.type === EventType.ScreenEvent) {
68+
const properties = await getSessionReplayProperties();
69+
event.properties = { ...event.properties, ...properties };
70+
}
71+
72+
return event;
73+
}
74+
75+
async shutdown(): Promise<void> {
76+
await this.initPromise;
77+
await stop();
78+
}
79+
80+
async start(): Promise<void> {
81+
await this.initPromise;
82+
await start();
83+
}
84+
85+
async stop(): Promise<void> {
86+
await this.initPromise;
87+
await stop();
88+
}
89+
}
90+
91+
export function createSegmentSessionReplayPlugin(config: SessionReplayConfig): Plugin {
92+
return new SegmentSessionReplayPlugin(config);
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const VERSION = '0.0.1-beta.1';

0 commit comments

Comments
 (0)