Skip to content

Commit 92357ad

Browse files
author
Kelly Wallach
committed
feat(session replay): update targeting logic to be compatible with new idb store format
1 parent e11650c commit 92357ad

File tree

4 files changed

+139
-13
lines changed

4 files changed

+139
-13
lines changed

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export class SessionReplayJoinedConfigGenerator {
4848
sessionId,
4949
);
5050

51+
const targetingConfig = await this.remoteConfigFetch.getRemoteConfig(
52+
'sessionReplay',
53+
'sr_targeting_config',
54+
sessionId,
55+
);
56+
5157
if (samplingConfig || privacyConfig) {
5258
remoteConfig = {};
5359
if (samplingConfig) {
@@ -56,6 +62,9 @@ export class SessionReplayJoinedConfigGenerator {
5662
if (privacyConfig) {
5763
remoteConfig.sr_privacy_config = privacyConfig;
5864
}
65+
if (targetingConfig) {
66+
remoteConfig.sr_targeting_config = targetingConfig;
67+
}
5968
}
6069
} catch (err: unknown) {
6170
const knownError = err as Error;
@@ -67,7 +76,11 @@ export class SessionReplayJoinedConfigGenerator {
6776
return config;
6877
}
6978

70-
const { sr_sampling_config: samplingConfig, sr_privacy_config: remotePrivacyConfig } = remoteConfig;
79+
const {
80+
sr_sampling_config: samplingConfig,
81+
sr_privacy_config: privacyConfig,
82+
sr_targeting_config: targetingConfig,
83+
} = remoteConfig;
7184
if (samplingConfig && Object.keys(samplingConfig).length > 0) {
7285
if (Object.prototype.hasOwnProperty.call(samplingConfig, 'capture_enabled')) {
7386
config.captureEnabled = samplingConfig.capture_enabled;
@@ -148,6 +161,10 @@ export class SessionReplayJoinedConfigGenerator {
148161
config.privacyConfig = joinedPrivacyConfig;
149162
}
150163

164+
if (targetingConfig && Object.keys(targetingConfig).length > 0) {
165+
config.targetingConfig = targetingConfig;
166+
}
167+
151168
return config;
152169
}
153170
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { Config, LogLevel, Logger } from '@amplitude/analytics-types';
2+
import { TargetingFlag } from '@amplitude/targeting';
23

34
export interface SamplingConfig {
45
sample_rate: number;
56
capture_enabled: boolean;
67
}
78

9+
export type TargetingConfig = TargetingFlag;
10+
811
export type SessionReplayRemoteConfig = {
912
sr_sampling_config?: SamplingConfig;
1013
sr_privacy_config?: PrivacyConfig;
14+
sr_targeting_config?: TargetingConfig;
1115
};
1216

1317
export interface SessionReplayRemoteConfigAPIResponse {
@@ -45,6 +49,7 @@ export interface SessionReplayLocalConfig extends Config {
4549

4650
export interface SessionReplayJoinedConfig extends SessionReplayLocalConfig {
4751
captureEnabled?: boolean;
52+
targetingConfig?: TargetingConfig;
4853
}
4954

5055
export interface SessionReplayRemoteConfigFetch {

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

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getAnalyticsConnector, getGlobalScope } from '@amplitude/analytics-clie
22
import { Logger, returnWrapper } from '@amplitude/analytics-core';
33
import { Logger as ILogger } from '@amplitude/analytics-types';
44
import { pack, record } from '@amplitude/rrweb';
5-
import { TargetingParameters } from '@amplitude/targeting';
5+
import { TargetingParameters, evaluateTargeting } from '@amplitude/targeting';
66
import { createSessionReplayJoinedConfigGenerator } from './config/joined-config';
77
import { SessionReplayJoinedConfig, SessionReplayJoinedConfigGenerator } from './config/types';
88
import {
@@ -14,11 +14,10 @@ import {
1414
import { createEventsManager } from './events/events-manager';
1515
import { generateHashCode, isSessionInSample, maskFn } from './helpers';
1616
import { SessionIdentifiers } from './identifiers';
17+
import * as TargetingIDBStore from './targeting-idb-store';
1718
import {
1819
AmplitudeSessionReplay,
1920
SessionReplayEventsManager as AmplitudeSessionReplayEventsManager,
20-
SessionReplayRemoteConfigFetch as AmplitudeSessionReplayRemoteConfigFetch,
21-
SessionReplaySessionIDBStore as AmplitudeSessionReplaySessionIDBStore,
2221
SessionIdentifiers as ISessionIdentifiers,
2322
SessionReplayOptions,
2423
} from './typings/session-replay';
@@ -28,11 +27,10 @@ export class SessionReplay implements AmplitudeSessionReplay {
2827
config: SessionReplayJoinedConfig | undefined;
2928
joinedConfigGenerator: SessionReplayJoinedConfigGenerator | undefined;
3029
identifiers: ISessionIdentifiers | undefined;
31-
remoteConfigFetch: AmplitudeSessionReplayRemoteConfigFetch | undefined;
3230
eventsManager: AmplitudeSessionReplayEventsManager | undefined;
33-
sessionIDBStore: AmplitudeSessionReplaySessionIDBStore | undefined;
3431
loggerProvider: ILogger;
3532
recordCancelCallback: ReturnType<typeof record> | null = null;
33+
sessionTargetingMatch = false;
3634

3735
constructor() {
3836
this.loggerProvider = new Logger();
@@ -104,7 +102,7 @@ export class SessionReplay implements AmplitudeSessionReplay {
104102
}
105103

106104
if (globalScope && globalScope.document && globalScope.document.hasFocus()) {
107-
this.initialize(true);
105+
await this.initialize(true);
108106
}
109107
}
110108

@@ -171,20 +169,51 @@ export class SessionReplay implements AmplitudeSessionReplay {
171169
};
172170

173171
focusListener = () => {
174-
this.initialize();
172+
void this.initialize();
175173
};
176174

177175
evaluateTargeting = async (targetingParams?: Pick<TargetingParameters, 'event' | 'userProperties'>) => {
178-
if (!this.identifiers || !this.identifiers.sessionId || !this.remoteConfigFetch || !this.config) {
176+
if (!this.identifiers || !this.identifiers.sessionId || !this.config) {
179177
this.loggerProvider.error('Session replay init has not been called, cannot evaluate targeting.');
180178
return;
181179
}
182180

183-
await this.remoteConfigFetch.evaluateTargeting({
181+
const idbTargetingMatch = await TargetingIDBStore.getTargetingMatchForSession({
182+
loggerProvider: this.config.loggerProvider,
183+
apiKey: this.config.apiKey,
184184
sessionId: this.identifiers.sessionId,
185-
deviceId: this.getDeviceId(),
186-
...targetingParams,
187185
});
186+
if (idbTargetingMatch === true) {
187+
this.sessionTargetingMatch = true;
188+
return;
189+
}
190+
191+
// Finally evaluate targeting if previous two checks were false or undefined
192+
try {
193+
if (this.config.targetingConfig) {
194+
const targetingResult = evaluateTargeting({
195+
...targetingParams,
196+
flag: this.config.targetingConfig,
197+
sessionId: this.identifiers.sessionId,
198+
});
199+
this.sessionTargetingMatch =
200+
this.sessionTargetingMatch === false && targetingResult.sr_targeting_config.key === 'on';
201+
} else {
202+
// If the targeting config is undefined or an empty object,
203+
// assume the response was valid but no conditions were set,
204+
// so all users match targeting
205+
this.sessionTargetingMatch = true;
206+
}
207+
void TargetingIDBStore.storeTargetingMatchForSession({
208+
loggerProvider: this.config.loggerProvider,
209+
apiKey: this.config.apiKey,
210+
sessionId: this.identifiers.sessionId,
211+
targetingMatch: this.sessionTargetingMatch,
212+
});
213+
} catch (err: unknown) {
214+
const knownError = err as Error;
215+
this.config.loggerProvider.warn(knownError.message);
216+
}
188217
};
189218

190219
stopRecordingAndSendEvents(sessionId?: number) {
@@ -198,7 +227,7 @@ export class SessionReplay implements AmplitudeSessionReplay {
198227
this.eventsManager.sendCurrentSequenceEvents({ sessionId: sessionIdToSend, deviceId });
199228
}
200229

201-
initialize(shouldSendStoredEvents = false) {
230+
async initialize(shouldSendStoredEvents = false) {
202231
if (!this.identifiers?.sessionId) {
203232
this.loggerProvider.log(`Session is not being recorded due to lack of session id.`);
204233
return;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Logger as ILogger } from '@amplitude/analytics-types';
2+
import { DBSchema, IDBPDatabase, openDB } from 'idb';
3+
import { STORAGE_FAILURE } from './messages';
4+
5+
export interface SessionReplayTargetingDB extends DBSchema {
6+
sessionTargetingMatch: {
7+
key: number;
8+
value: {
9+
sessionId: number;
10+
targetingMatch: boolean;
11+
};
12+
};
13+
}
14+
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+
};
26+
27+
const openOrCreateDB = async (apiKey: string) => {
28+
const dbName = `${apiKey.substring(0, 10)}_amp_session_replay_targeting`;
29+
return await createStore(dbName);
30+
};
31+
32+
export const getTargetingMatchForSession = async ({
33+
loggerProvider,
34+
apiKey,
35+
sessionId,
36+
}: {
37+
loggerProvider: ILogger;
38+
apiKey: string;
39+
sessionId: number;
40+
}) => {
41+
const db = await openOrCreateDB(apiKey);
42+
try {
43+
const targetingMatchForSession = await db?.get<'sessionTargetingMatch'>('sessionTargetingMatch', sessionId);
44+
45+
return targetingMatchForSession?.targetingMatch;
46+
} catch (e) {
47+
loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`);
48+
}
49+
return undefined;
50+
};
51+
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+
const db = await openOrCreateDB(apiKey);
64+
try {
65+
const targetingMatchForSession = await db?.put<'sessionTargetingMatch'>('sessionTargetingMatch', {
66+
targetingMatch,
67+
sessionId,
68+
});
69+
70+
return targetingMatchForSession;
71+
} catch (e) {
72+
loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`);
73+
}
74+
return undefined;
75+
};

0 commit comments

Comments
 (0)