Skip to content

Commit

Permalink
fix(session replay): rebase fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Kelly Wallach committed Jul 24, 2024
1 parent 68dff21 commit cdab2ae
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 411 deletions.
4 changes: 3 additions & 1 deletion packages/plugin-session-replay-browser/src/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ class SessionReplayEnrichmentPlugin implements EnrichmentPlugin {
const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore;
userProperties = identityStore.getIdentity().userProperties;
}
await sessionReplay.setSessionId(this.config.sessionId, this.config.deviceId, { userProperties }).promise;
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
Expand Down
6 changes: 5 additions & 1 deletion packages/session-replay-browser/src/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ export class SessionReplay implements AmplitudeSessionReplay {

evaluateTargetingAndRecord = async (targetingParams?: Pick<TargetingParameters, 'event' | 'userProperties'>) => {
if (!this.identifiers || !this.identifiers.sessionId || !this.config) {
this.loggerProvider.error('Session replay init has not been called, cannot evaluate targeting.');
if (!this.identifiers?.sessionId) {
this.loggerProvider.warn('Session ID has not been set, cannot evaluate targeting for Session Replay.');
} else {
this.loggerProvider.warn('Session replay init has not been called, cannot evaluate targeting.');
}
return false;
}
// Return early if a targeting match has already been made
Expand Down
1 change: 1 addition & 0 deletions packages/targeting/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ module.exports = {
rootDir: '.',
testEnvironment: 'jsdom',
coveragePathIgnorePatterns: ['index.ts'],
setupFilesAfterEnv: ['./test/jest-setup.js'],
};
2 changes: 2 additions & 0 deletions packages/targeting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
"@amplitude/analytics-core": ">=1 <3",
"@amplitude/analytics-types": ">=1 <3",
"@amplitude/experiment-core": "0.7.2",
"idb": "^8.0.0",
"tslib": "^2.4.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^10.0.1",
"fake-indexeddb": "4.0.2",
"rollup": "^2.79.1",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-gzip": "^3.1.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/targeting/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { Targeting } from './targeting';
import targeting from './targeting-factory';
export const { evaluateTargeting } = targeting;
export { TargetingFlag, TargetingParameters } from './typings/targeting';
183 changes: 0 additions & 183 deletions packages/targeting/src/targeting-idb-store.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,25 @@
<<<<<<< HEAD
<<<<<<< HEAD
import { Logger as ILogger } from '@amplitude/analytics-types';
=======
import { Logger as ILogger, SpecialEventType } from '@amplitude/analytics-types';
>>>>>>> 3e83ab49 (feat(session replay): add ability to target on multiple events)
=======
import { Logger as ILogger } from '@amplitude/analytics-types';
>>>>>>> 79705348 (test(targeting + session replay): get test coverage up to 100%)
import { DBSchema, IDBPDatabase, IDBPTransaction, openDB } from 'idb';

export const MAX_IDB_STORAGE_LENGTH = 1000 * 60 * 60 * 24 * 2; // 2 days

<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 7ea29d5c (fix(targeting): keep track of open db instances and ensure deduplication of events)
// This type is constructed to allow for future proofing - in the future we may want
// to track how many of each event is fired, and we may want to track event properties
// Any further fields, like event properties, can be added to this type without causing
// a breaking change
type EventData = { event_type: string };

type EventTypeStore = { [event_type: string]: { [timestamp: number]: EventData } };
<<<<<<< HEAD
=======
>>>>>>> 3e83ab49 (feat(session replay): add ability to target on multiple events)
=======
>>>>>>> 7ea29d5c (fix(targeting): keep track of open db instances and ensure deduplication of events)
export interface TargetingDB extends DBSchema {
eventTypesForSession: {
key: number;
value: {
sessionId: number;
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
eventTypes: EventTypeStore;
=======
eventTypes: Set<string>;
>>>>>>> 3e83ab49 (feat(session replay): add ability to target on multiple events)
=======
eventTypes: Array<{ event_type: string }>;
>>>>>>> 79705348 (test(targeting + session replay): get test coverage up to 100%)
=======
eventTypes: EventTypeStore;
>>>>>>> 7ea29d5c (fix(targeting): keep track of open db instances and ensure deduplication of events)
};
};
}

<<<<<<< HEAD
<<<<<<< HEAD
export class TargetingIDBStore {
dbs: { [apiKey: string]: IDBPDatabase<TargetingDB> } = {};

Expand Down Expand Up @@ -177,155 +146,3 @@ export class TargetingIDBStore {
}

export const targetingIDBStore = new TargetingIDBStore();
=======
export const createStore = async (dbName: string) => {
return await openDB<TargetingDB>(dbName, 1, {
upgrade: (db: IDBPDatabase<TargetingDB>) => {
if (!db.objectStoreNames.contains('eventTypesForSession')) {
db.createObjectStore('eventTypesForSession', {
keyPath: 'sessionId',
});
}
},
});
};
=======
export class TargetingIDBStore {
dbs: { [apiKey: string]: IDBPDatabase<TargetingDB> } | undefined;
>>>>>>> 7ea29d5c (fix(targeting): keep track of open db instances and ensure deduplication of events)

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

openOrCreateDB = async (apiKey: string) => {
if (this.dbs && this.dbs[apiKey]) {
return this.dbs[apiKey];
}
const dbName = `${apiKey.substring(0, 10)}_amp_targeting`;
const db = await this.createStore(dbName);
this.dbs = {
...this.dbs,
[apiKey]: db,
};
return db;
};

updateEventListForSession = async ({
sessionId,
eventType,
eventTime,
loggerProvider,
tx,
}: {
sessionId: number;
eventType: string;
eventTime: number;
loggerProvider: ILogger;
tx: IDBPTransaction<TargetingDB, ['eventTypesForSession'], 'readwrite'>;
}) => {
try {
const eventTypesForSessionStorage = await tx.store.get(sessionId);
const eventTypesForSession = eventTypesForSessionStorage ? eventTypesForSessionStorage.eventTypes : {};
const eventTypeStore = eventTypesForSession[eventType] || {};

const updatedEventTypes: EventTypeStore = {
...eventTypesForSession,
[eventType]: {
...eventTypeStore,
[eventTime]: { event_type: eventType },
},
};
await tx.store.put({ sessionId, eventTypes: updatedEventTypes });
return updatedEventTypes;
} catch (e) {
loggerProvider.warn(`Failed to store events for targeting ${sessionId}: ${e as string}`);
}
return undefined;
};

deleteOldSessionEventTypes = async ({
currentSessionId,
loggerProvider,
tx,
}: {
currentSessionId: number;
loggerProvider: ILogger;
tx: IDBPTransaction<TargetingDB, ['eventTypesForSession'], 'readwrite'>;
}) => {
try {
const allEventTypeObjs = await tx.store.getAll();
for (let i = 0; i < allEventTypeObjs.length; i++) {
const eventTypeObj = allEventTypeObjs[i];
const amountOfTimeSinceSession = Date.now() - eventTypeObj.sessionId;
if (eventTypeObj.sessionId !== currentSessionId && amountOfTimeSinceSession > MAX_IDB_STORAGE_LENGTH) {
await tx.store.delete(eventTypeObj.sessionId);
}
}
} catch (e) {
loggerProvider.warn(`Failed to clear old session events for targeting: ${e as string}`);
}
};

storeEventTypeForSession = async ({
loggerProvider,
sessionId,
eventType,
eventTime,
apiKey,
}: {
loggerProvider: ILogger;
apiKey: string;
eventType: string;
eventTime: number;
sessionId: number;
}) => {
try {
const db = await this.openOrCreateDB(apiKey);

const tx = db.transaction<'eventTypesForSession', 'readwrite'>('eventTypesForSession', 'readwrite');
if (!tx) {
return;
}

<<<<<<< HEAD
return updatedEventTypes;
} catch (e) {
loggerProvider.warn(`Failed to store events for targeting ${sessionId}: ${e as string}`);
}
return undefined;
};
>>>>>>> 3e83ab49 (feat(session replay): add ability to target on multiple events)
=======
// Update the list of events for the session
const updatedEventTypes = await this.updateEventListForSession({
sessionId,
tx,
loggerProvider,
eventType,
eventTime,
});

// Clear out sessions older than 2 days
await this.deleteOldSessionEventTypes({ currentSessionId: sessionId, tx, loggerProvider });

await tx.done;

return updatedEventTypes;
} catch (e) {
loggerProvider.warn(`Failed to store events for targeting ${sessionId}: ${e as string}`);
}
return undefined;
};
}

export const targetingIDBStore = new TargetingIDBStore();
>>>>>>> 7ea29d5c (fix(targeting): keep track of open db instances and ensure deduplication of events)
30 changes: 27 additions & 3 deletions packages/targeting/src/targeting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EvaluationEngine } from '@amplitude/experiment-core';
import { targetingIDBStore } from './targeting-idb-store';
import { Targeting as AmplitudeTargeting, TargetingParameters } from './typings/targeting';

export class Targeting implements AmplitudeTargeting {
Expand All @@ -8,15 +9,38 @@ export class Targeting implements AmplitudeTargeting {
this.evaluationEngine = new EvaluationEngine();
}

evaluateTargeting({ event, sessionId, userProperties, deviceId, flag }: TargetingParameters) {
evaluateTargeting = async ({
apiKey,
loggerProvider,
event,
sessionId,
userProperties,
deviceId,
flag,
}: TargetingParameters) => {
const eventTypes =
event && event.time
? await targetingIDBStore.storeEventTypeForSession({
loggerProvider: loggerProvider,
apiKey: apiKey,
sessionId,
eventType: event.event_type,
eventTime: event.time,
})
: undefined;

const eventStrings = eventTypes && new Set(Object.keys(eventTypes));

const context = {
session_id: sessionId,
event,
event_types: eventStrings && Array.from(eventStrings),
user: {
device_id: deviceId,
user_properties: userProperties,
},
};
return this.evaluationEngine.evaluate(context, [flag]);
}
const targetingBucket = this.evaluationEngine.evaluate(context, [flag]);
return targetingBucket;
};
}
12 changes: 8 additions & 4 deletions packages/targeting/src/typings/targeting.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Event, IdentifyUserProperties } from '@amplitude/analytics-types';
import { Event, Logger } from '@amplitude/analytics-types';
import { EvaluationFlag, EvaluationVariant } from '@amplitude/experiment-core';

export type TargetingFlag = EvaluationFlag;
export interface TargetingParameters {
event?: Event;
userProperties?: IdentifyUserProperties;
userProperties?: { [key: string]: any };
deviceId?: string;
flag: EvaluationFlag;
sessionId?: string;
sessionId: number;
apiKey: string;
loggerProvider: Logger;
}

export interface Targeting {
evaluateTargeting(args: TargetingParameters): Record<string, EvaluationVariant>;
evaluateTargeting: (args: TargetingParameters) => Promise<Record<string, EvaluationVariant>>;
}
27 changes: 27 additions & 0 deletions packages/targeting/test/flag-config-data/catch-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ export const flagCatchAll = {
],
],
},
{
metadata: { segmentName: 'multiple event trigger' },
bucket: {
selector: ['context', 'session_id'],
salt: 'xdfrewd', // Different salt for each bucket to allow for fallthrough
allocations: [
{
range: [0, 19], // Selects 20% of users that match these conditions
distributions: [
{
variant: 'on',
range: [0, 42949673],
},
],
},
],
},
conditions: [
[
{
selector: ['context', 'event_types'],
op: 'set contains',
values: ['Add to Cart', 'Purchase'],
},
],
],
},
{
metadata: { segmentName: 'user property' },
bucket: {
Expand Down
Loading

0 comments on commit cdab2ae

Please sign in to comment.