Skip to content

Commit 9426ee0

Browse files
author
Kelly Wallach
committed
fix(session replay): pr feedback
1 parent a589288 commit 9426ee0

File tree

7 files changed

+198
-240
lines changed

7 files changed

+198
-240
lines changed

packages/session-replay-browser/README.md

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

50-
### 3. Evaluate targeting and get session replay event properties
51-
Any event that occurs within the span of a session replay must be passed to the SDK to evaluate against targeting conditions. It must also 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.
5252
```typescript
5353
const sessionTargetingMatch = sessionReplay.evaluateTargetingAndRecord({ event: {
5454
event_type: EVENT_NAME,
55+
time: EVENT_TIMESTAMP,
5556
event_properties: eventProperties
5657
} });
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.
62+
```typescript
5763
const sessionReplayProperties = sessionReplay.getSessionReplayProperties();
5864
track(EVENT_NAME, {
5965
...eventProperties,
6066
...sessionReplayProperties
6167
})
6268
```
6369

64-
### 4. Update session id
70+
### 5. Update session id
6571
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:
6672
```typescript
6773
sessionReplay.setSessionId(UNIX_TIMESTAMP)
@@ -71,7 +77,7 @@ You can optionally pass a new device id as a second argument as well:
7177
sessionReplay.setSessionId(UNIX_TIMESTAMP, deviceId)
7278
```
7379

74-
### 5. Shutdown (optional)
80+
### 6. Shutdown (optional)
7581
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.
7682
```typescript
7783
sessionReplay.shutdown()

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

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ export class SessionReplay implements AmplitudeSessionReplay {
115115

116116
this.eventsManager = new MultiEventManager<'replay' | 'interaction', string>(...managers);
117117

118+
this.identifiers.deviceId &&
119+
void this.eventsManager.sendStoredEvents({
120+
deviceId: this.identifiers.deviceId,
121+
});
122+
118123
this.loggerProvider.log('Installing @amplitude/session-replay-browser.');
119124

120125
const globalScope = getGlobalScope();
@@ -126,7 +131,7 @@ export class SessionReplay implements AmplitudeSessionReplay {
126131
}
127132

128133
if (globalScope && globalScope.document && globalScope.document.hasFocus()) {
129-
await this.initialize(true);
134+
await this.evaluateTargetingAndRecord();
130135
}
131136
}
132137

@@ -193,20 +198,28 @@ export class SessionReplay implements AmplitudeSessionReplay {
193198
};
194199

195200
focusListener = () => {
196-
void this.initialize();
201+
if (this.sessionTargetingMatch) {
202+
this.recordEvents();
203+
}
197204
};
198205

199206
evaluateTargetingAndRecord = async (options?: { event?: Event }) => {
200207
if (!this.identifiers || !this.identifiers.sessionId || !this.config) {
201208
this.loggerProvider.error('Session replay init has not been called, cannot evaluate targeting.');
202209
return false;
203210
}
204-
let eventForTargeting = options?.event;
211+
// Return early if a targeting match has already been made
212+
if (this.sessionTargetingMatch) {
213+
if (!this.recordCancelCallback) {
214+
this.recordEvents();
215+
}
216+
return this.sessionTargetingMatch;
217+
}
205218

219+
let eventForTargeting = options?.event;
206220
if (
207-
[SpecialEventType.GROUP_IDENTIFY, SpecialEventType.IDENTIFY, SpecialEventType.REVENUE].includes(
208-
eventForTargeting?.event_type as SpecialEventType,
209-
)
221+
eventForTargeting &&
222+
Object.values(SpecialEventType).includes(eventForTargeting.event_type as SpecialEventType)
210223
) {
211224
eventForTargeting = undefined;
212225
}
@@ -240,22 +253,6 @@ export class SessionReplay implements AmplitudeSessionReplay {
240253
this.eventsManager.sendCurrentSequenceEvents({ sessionId: sessionIdToSend, deviceId });
241254
}
242255

243-
async initialize(shouldSendStoredEvents = false) {
244-
if (!this.identifiers?.sessionId) {
245-
this.loggerProvider.log(`Session is not being recorded due to lack of session id.`);
246-
return;
247-
}
248-
249-
const deviceId = this.getDeviceId();
250-
if (!deviceId) {
251-
this.loggerProvider.log(`Session is not being recorded due to lack of device id.`);
252-
return;
253-
}
254-
this.eventsManager && shouldSendStoredEvents && this.eventsManager.sendStoredEvents({ deviceId });
255-
256-
await this.evaluateTargetingAndRecord();
257-
}
258-
259256
shouldOptOut() {
260257
let identityStoreOptOut: boolean | undefined;
261258
if (this.config?.instanceName) {

packages/session-replay-browser/src/targeting/targeting-idb-store.ts

Lines changed: 92 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,90 +12,103 @@ export interface SessionReplayTargetingDB extends DBSchema {
1212
};
1313
}
1414

15-
export const createStore = async (dbName: string) => {
16-
return await openDB<SessionReplayTargetingDB>(dbName, 1, {
17-
upgrade: (db: IDBPDatabase<SessionReplayTargetingDB>) => {
18-
if (!db.objectStoreNames.contains('sessionTargetingMatch')) {
19-
db.createObjectStore('sessionTargetingMatch', {
20-
keyPath: 'sessionId',
21-
});
22-
}
23-
},
24-
});
25-
};
15+
export class TargetingIDBStore {
16+
dbs: { [apiKey: string]: IDBPDatabase<SessionReplayTargetingDB> } | undefined;
2617

27-
const openOrCreateDB = async (apiKey: string) => {
28-
const dbName = `${apiKey.substring(0, 10)}_amp_session_replay_targeting`;
29-
return await createStore(dbName);
30-
};
18+
createStore = async (dbName: string) => {
19+
return await openDB<SessionReplayTargetingDB>(dbName, 1, {
20+
upgrade: (db: IDBPDatabase<SessionReplayTargetingDB>) => {
21+
if (!db.objectStoreNames.contains('sessionTargetingMatch')) {
22+
db.createObjectStore('sessionTargetingMatch', {
23+
keyPath: 'sessionId',
24+
});
25+
}
26+
},
27+
});
28+
};
3129

32-
export const getTargetingMatchForSession = async ({
33-
loggerProvider,
34-
apiKey,
35-
sessionId,
36-
}: {
37-
loggerProvider: ILogger;
38-
apiKey: string;
39-
sessionId: number;
40-
}) => {
41-
try {
42-
const db = await openOrCreateDB(apiKey);
43-
const targetingMatchForSession = await db.get<'sessionTargetingMatch'>('sessionTargetingMatch', sessionId);
30+
openOrCreateDB = async (apiKey: string) => {
31+
if (this.dbs && this.dbs[apiKey]) {
32+
return this.dbs[apiKey];
33+
}
34+
const dbName = `${apiKey.substring(0, 10)}_amp_session_replay_targeting`;
35+
const db = await this.createStore(dbName);
36+
this.dbs = {
37+
...this.dbs,
38+
[apiKey]: db,
39+
};
40+
return db;
41+
};
4442

45-
return targetingMatchForSession?.targetingMatch;
46-
} catch (e) {
47-
loggerProvider.warn(`Failed to get targeting match for session id ${sessionId}: ${e as string}`);
48-
}
49-
return undefined;
50-
};
43+
getTargetingMatchForSession = async ({
44+
loggerProvider,
45+
apiKey,
46+
sessionId,
47+
}: {
48+
loggerProvider: ILogger;
49+
apiKey: string;
50+
sessionId: number;
51+
}) => {
52+
try {
53+
const db = await this.openOrCreateDB(apiKey);
54+
const targetingMatchForSession = await db.get<'sessionTargetingMatch'>('sessionTargetingMatch', sessionId);
5155

52-
export const storeTargetingMatchForSession = async ({
53-
loggerProvider,
54-
apiKey,
55-
sessionId,
56-
targetingMatch,
57-
}: {
58-
loggerProvider: ILogger;
59-
apiKey: string;
60-
sessionId: number;
61-
targetingMatch: boolean;
62-
}) => {
63-
try {
64-
const db = await openOrCreateDB(apiKey);
65-
const targetingMatchForSession = await db.put<'sessionTargetingMatch'>('sessionTargetingMatch', {
66-
targetingMatch,
67-
sessionId,
68-
});
56+
return targetingMatchForSession?.targetingMatch;
57+
} catch (e) {
58+
loggerProvider.warn(`Failed to get targeting match for session id ${sessionId}: ${e as string}`);
59+
}
60+
return undefined;
61+
};
6962

70-
return targetingMatchForSession;
71-
} catch (e) {
72-
loggerProvider.warn(`Failed to store targeting match for session id ${sessionId}: ${e as string}`);
73-
}
74-
return undefined;
75-
};
63+
storeTargetingMatchForSession = async ({
64+
loggerProvider,
65+
apiKey,
66+
sessionId,
67+
targetingMatch,
68+
}: {
69+
loggerProvider: ILogger;
70+
apiKey: string;
71+
sessionId: number;
72+
targetingMatch: boolean;
73+
}) => {
74+
try {
75+
const db = await this.openOrCreateDB(apiKey);
76+
const targetingMatchForSession = await db.put<'sessionTargetingMatch'>('sessionTargetingMatch', {
77+
targetingMatch,
78+
sessionId,
79+
});
7680

77-
export const clearStoreOfOldSessions = async ({
78-
loggerProvider,
79-
apiKey,
80-
currentSessionId,
81-
}: {
82-
loggerProvider: ILogger;
83-
apiKey: string;
84-
currentSessionId: number;
85-
}) => {
86-
try {
87-
const db = await openOrCreateDB(apiKey);
88-
const tx = db.transaction<'sessionTargetingMatch', 'readwrite'>('sessionTargetingMatch', 'readwrite');
89-
const allTargetingMatchObjs = await tx.store.getAll();
90-
for (let i = 0; i < allTargetingMatchObjs.length; i++) {
91-
const targetingMatchObj = allTargetingMatchObjs[i];
92-
const amountOfTimeSinceSession = Date.now() - targetingMatchObj.sessionId;
93-
if (targetingMatchObj.sessionId !== currentSessionId && amountOfTimeSinceSession > MAX_IDB_STORAGE_LENGTH) {
94-
await tx.store.delete(targetingMatchObj.sessionId);
81+
return targetingMatchForSession;
82+
} catch (e) {
83+
loggerProvider.warn(`Failed to store targeting match for session id ${sessionId}: ${e as string}`);
84+
}
85+
return undefined;
86+
};
87+
88+
clearStoreOfOldSessions = async ({
89+
loggerProvider,
90+
apiKey,
91+
currentSessionId,
92+
}: {
93+
loggerProvider: ILogger;
94+
apiKey: string;
95+
currentSessionId: number;
96+
}) => {
97+
try {
98+
const db = await this.openOrCreateDB(apiKey);
99+
const tx = db.transaction<'sessionTargetingMatch', 'readwrite'>('sessionTargetingMatch', 'readwrite');
100+
const allTargetingMatchObjs = await tx.store.getAll();
101+
for (let i = 0; i < allTargetingMatchObjs.length; i++) {
102+
const targetingMatchObj = allTargetingMatchObjs[i];
103+
const amountOfTimeSinceSession = Date.now() - targetingMatchObj.sessionId;
104+
if (targetingMatchObj.sessionId !== currentSessionId && amountOfTimeSinceSession > MAX_IDB_STORAGE_LENGTH) {
105+
await tx.store.delete(targetingMatchObj.sessionId);
106+
}
95107
}
108+
await tx.done;
109+
} catch (e) {
110+
loggerProvider.warn(`Failed to clear old targeting matches for sessions: ${e as string}`);
96111
}
97-
await tx.done;
98-
} catch (e) {
99-
loggerProvider.warn(`Failed to clear old targeting matches for sessions: ${e as string}`);
100-
}
101-
};
112+
};
113+
}
114+
export const targetingIDBStore = new TargetingIDBStore();

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TargetingParameters, evaluateTargeting as evaluateTargetingPackage } from '@amplitude/targeting';
22
import { SessionReplayJoinedConfig } from 'src/config/types';
3-
import * as TargetingIDBStore from './targeting-idb-store';
3+
import { targetingIDBStore } from './targeting-idb-store';
44

55
export const evaluateTargetingAndStore = async ({
66
sessionId,
@@ -11,13 +11,13 @@ export const evaluateTargetingAndStore = async ({
1111
config: SessionReplayJoinedConfig;
1212
targetingParams?: Pick<TargetingParameters, 'event' | 'userProperties'>;
1313
}) => {
14-
await TargetingIDBStore.clearStoreOfOldSessions({
14+
await targetingIDBStore.clearStoreOfOldSessions({
1515
loggerProvider: config.loggerProvider,
1616
apiKey: config.apiKey,
1717
currentSessionId: sessionId,
1818
});
1919

20-
const idbTargetingMatch = await TargetingIDBStore.getTargetingMatchForSession({
20+
const idbTargetingMatch = await targetingIDBStore.getTargetingMatchForSession({
2121
loggerProvider: config.loggerProvider,
2222
apiKey: config.apiKey,
2323
sessionId: sessionId,
@@ -41,7 +41,7 @@ export const evaluateTargetingAndStore = async ({
4141
});
4242
sessionTargetingMatch = targetingResult.sr_targeting_config.key === 'on';
4343
}
44-
void TargetingIDBStore.storeTargetingMatchForSession({
44+
void targetingIDBStore.storeTargetingMatchForSession({
4545
loggerProvider: config.loggerProvider,
4646
apiKey: config.apiKey,
4747
sessionId: sessionId,

0 commit comments

Comments
 (0)