Skip to content

Commit

Permalink
feat(session replay): update targeting logic to be compatible with ne…
Browse files Browse the repository at this point in the history
…w idb store format
  • Loading branch information
Kelly Wallach committed Jun 12, 2024
1 parent 049d56c commit 8a38a38
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 13 deletions.
19 changes: 18 additions & 1 deletion packages/session-replay-browser/src/config/joined-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export class SessionReplayJoinedConfigGenerator {
sessionId,
);

const targetingConfig = await this.remoteConfigFetch.getRemoteConfig(
'sessionReplay',
'sr_targeting_config',
sessionId,
);

if (samplingConfig || privacyConfig) {
remoteConfig = {};
if (samplingConfig) {
Expand All @@ -56,6 +62,9 @@ export class SessionReplayJoinedConfigGenerator {
if (privacyConfig) {
remoteConfig.sr_privacy_config = privacyConfig;
}
if (targetingConfig) {
remoteConfig.sr_targeting_config = targetingConfig;
}
}
} catch (err: unknown) {
const knownError = err as Error;
Expand All @@ -67,7 +76,11 @@ export class SessionReplayJoinedConfigGenerator {
return config;
}

const { sr_sampling_config: samplingConfig, sr_privacy_config: remotePrivacyConfig } = remoteConfig;
const {
sr_sampling_config: samplingConfig,
sr_privacy_config: privacyConfig,
sr_targeting_config: targetingConfig,
} = remoteConfig;
if (samplingConfig && Object.keys(samplingConfig).length > 0) {
if (Object.prototype.hasOwnProperty.call(samplingConfig, 'capture_enabled')) {
config.captureEnabled = samplingConfig.capture_enabled;
Expand Down Expand Up @@ -148,6 +161,10 @@ export class SessionReplayJoinedConfigGenerator {
config.privacyConfig = joinedPrivacyConfig;
}

if (targetingConfig && Object.keys(targetingConfig).length > 0) {
config.targetingConfig = targetingConfig;
}

return config;
}
}
Expand Down
5 changes: 5 additions & 0 deletions packages/session-replay-browser/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Config, LogLevel, Logger } from '@amplitude/analytics-types';
import { TargetingFlag } from '@amplitude/targeting';

export interface SamplingConfig {
sample_rate: number;
capture_enabled: boolean;
}

export type TargetingConfig = TargetingFlag;

export type SessionReplayRemoteConfig = {
sr_sampling_config?: SamplingConfig;
sr_privacy_config?: PrivacyConfig;
sr_targeting_config?: TargetingConfig;
};

export interface SessionReplayRemoteConfigAPIResponse {
Expand Down Expand Up @@ -45,6 +49,7 @@ export interface SessionReplayLocalConfig extends Config {

export interface SessionReplayJoinedConfig extends SessionReplayLocalConfig {
captureEnabled?: boolean;
targetingConfig?: TargetingConfig;
}

export interface SessionReplayRemoteConfigFetch {
Expand Down
53 changes: 41 additions & 12 deletions packages/session-replay-browser/src/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getAnalyticsConnector, getGlobalScope } from '@amplitude/analytics-clie
import { Logger, returnWrapper } from '@amplitude/analytics-core';
import { Logger as ILogger } from '@amplitude/analytics-types';
import { pack, record } from '@amplitude/rrweb';
import { TargetingParameters } from '@amplitude/targeting';
import { TargetingParameters, evaluateTargeting } from '@amplitude/targeting';
import { createSessionReplayJoinedConfigGenerator } from './config/joined-config';
import { SessionReplayJoinedConfig, SessionReplayJoinedConfigGenerator } from './config/types';
import {
Expand All @@ -14,11 +14,10 @@ import {
import { createEventsManager } from './events/events-manager';
import { generateHashCode, isSessionInSample, maskFn } from './helpers';
import { SessionIdentifiers } from './identifiers';
import * as TargetingIDBStore from './targeting-idb-store';
import {
AmplitudeSessionReplay,
SessionReplayEventsManager as AmplitudeSessionReplayEventsManager,
SessionReplayRemoteConfigFetch as AmplitudeSessionReplayRemoteConfigFetch,
SessionReplaySessionIDBStore as AmplitudeSessionReplaySessionIDBStore,
SessionIdentifiers as ISessionIdentifiers,
SessionReplayOptions,
} from './typings/session-replay';
Expand All @@ -28,11 +27,10 @@ export class SessionReplay implements AmplitudeSessionReplay {
config: SessionReplayJoinedConfig | undefined;
joinedConfigGenerator: SessionReplayJoinedConfigGenerator | undefined;
identifiers: ISessionIdentifiers | undefined;
remoteConfigFetch: AmplitudeSessionReplayRemoteConfigFetch | undefined;
eventsManager: AmplitudeSessionReplayEventsManager | undefined;
sessionIDBStore: AmplitudeSessionReplaySessionIDBStore | undefined;
loggerProvider: ILogger;
recordCancelCallback: ReturnType<typeof record> | null = null;
sessionTargetingMatch = false;

constructor() {
this.loggerProvider = new Logger();
Expand Down Expand Up @@ -104,7 +102,7 @@ export class SessionReplay implements AmplitudeSessionReplay {
}

if (globalScope && globalScope.document && globalScope.document.hasFocus()) {
this.initialize(true);
await this.initialize(true);
}
}

Expand Down Expand Up @@ -171,20 +169,51 @@ export class SessionReplay implements AmplitudeSessionReplay {
};

focusListener = () => {
this.initialize();
void this.initialize();
};

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

await this.remoteConfigFetch.evaluateTargeting({
const idbTargetingMatch = await TargetingIDBStore.getTargetingMatchForSession({
loggerProvider: this.config.loggerProvider,
apiKey: this.config.apiKey,
sessionId: this.identifiers.sessionId,
deviceId: this.getDeviceId(),
...targetingParams,
});
if (idbTargetingMatch === true) {
this.sessionTargetingMatch = true;
return;
}

// Finally evaluate targeting if previous two checks were false or undefined
try {
if (this.config.targetingConfig) {
const targetingResult = evaluateTargeting({
...targetingParams,
flag: this.config.targetingConfig,
sessionId: this.identifiers.sessionId,
});
this.sessionTargetingMatch =
this.sessionTargetingMatch === false && targetingResult.sr_targeting_config.key === 'on';
} else {
// If the targeting config is undefined or an empty object,
// assume the response was valid but no conditions were set,
// so all users match targeting
this.sessionTargetingMatch = true;
}
void TargetingIDBStore.storeTargetingMatchForSession({
loggerProvider: this.config.loggerProvider,
apiKey: this.config.apiKey,
sessionId: this.identifiers.sessionId,
targetingMatch: this.sessionTargetingMatch,
});
} catch (err: unknown) {
const knownError = err as Error;
this.config.loggerProvider.warn(knownError.message);
}
};

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

initialize(shouldSendStoredEvents = false) {
async initialize(shouldSendStoredEvents = false) {
if (!this.identifiers?.sessionId) {
this.loggerProvider.log(`Session is not being recorded due to lack of session id.`);
return;
Expand Down
75 changes: 75 additions & 0 deletions packages/session-replay-browser/src/targeting-idb-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Logger as ILogger } from '@amplitude/analytics-types';
import { DBSchema, IDBPDatabase, openDB } from 'idb';
import { STORAGE_FAILURE } from './messages';

export interface SessionReplayTargetingDB extends DBSchema {
sessionTargetingMatch: {
key: number;
value: {
sessionId: number;
targetingMatch: boolean;
};
};
}

export const createStore = async (dbName: string) => {
return await openDB<SessionReplayTargetingDB>(dbName, 1, {
upgrade: (db: IDBPDatabase<SessionReplayTargetingDB>) => {
if (!db.objectStoreNames.contains('sessionTargetingMatch')) {
db.createObjectStore('sessionTargetingMatch', {
keyPath: 'sessionId',
});
}
},
});
};

const openOrCreateDB = async (apiKey: string) => {
const dbName = `${apiKey.substring(0, 10)}_amp_session_replay_targeting`;
return await createStore(dbName);
};

export const getTargetingMatchForSession = async ({
loggerProvider,
apiKey,
sessionId,
}: {
loggerProvider: ILogger;
apiKey: string;
sessionId: number;
}) => {
const db = await openOrCreateDB(apiKey);
try {
const targetingMatchForSession = await db?.get<'sessionTargetingMatch'>('sessionTargetingMatch', sessionId);

return targetingMatchForSession?.targetingMatch;
} catch (e) {
loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`);
}
return undefined;
};

export const storeTargetingMatchForSession = async ({
loggerProvider,
apiKey,
sessionId,
targetingMatch,
}: {
loggerProvider: ILogger;
apiKey: string;
sessionId: number;
targetingMatch: boolean;
}) => {
const db = await openOrCreateDB(apiKey);
try {
const targetingMatchForSession = await db?.put<'sessionTargetingMatch'>('sessionTargetingMatch', {
targetingMatch,
sessionId,
});

return targetingMatchForSession;
} catch (e) {
loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`);
}
return undefined;
};

0 comments on commit 8a38a38

Please sign in to comment.