From 3daf644e3fa471d88c5083375d745ace52b9f4f0 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 8 Aug 2024 14:44:57 -0400 Subject: [PATCH 1/3] feat(session replay): add ability to capture replays based on targeting via remote config --- .../CHANGELOG.md | 19 +- .../package.json | 6 +- .../src/constants.ts | 11 + .../src/helpers.ts | 22 + .../src/session-replay.ts | 19 +- .../src/version.ts | 2 +- .../test/helpers.test.ts | 40 ++ .../test/session-replay.test.ts | 129 ++++- packages/session-replay-browser/README.md | 18 +- packages/session-replay-browser/package.json | 3 +- .../src/config/joined-config.ts | 19 +- .../src/config/types.ts | 5 + packages/session-replay-browser/src/index.ts | 10 +- .../src/session-replay-factory.ts | 11 +- .../src/session-replay.ts | 135 +++-- .../src/targeting/targeting-idb-store.ts | 111 ++++ .../src/targeting/targeting-manager.ts | 61 ++ .../src/typings/session-replay.ts | 10 +- .../session-replay-browser/src/version.ts | 2 +- .../test/flag-config-data.ts | 84 +++ .../test/integration/sampling.test.ts | 71 ++- .../test/session-replay.test.ts | 349 +++++++----- .../targeting/targeting-idb-store.test.ts | 186 ++++++ .../test/targeting/targeting-manager.test.ts | 135 +++++ yarn.lock | 529 ++---------------- 25 files changed, 1287 insertions(+), 700 deletions(-) create mode 100644 packages/plugin-session-replay-browser/src/helpers.ts create mode 100644 packages/plugin-session-replay-browser/test/helpers.test.ts create mode 100644 packages/session-replay-browser/src/targeting/targeting-idb-store.ts create mode 100644 packages/session-replay-browser/src/targeting/targeting-manager.ts create mode 100644 packages/session-replay-browser/test/flag-config-data.ts create mode 100644 packages/session-replay-browser/test/targeting/targeting-idb-store.test.ts create mode 100644 packages/session-replay-browser/test/targeting/targeting-manager.test.ts diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 4de2c64f6..152f17431 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -41,16 +41,21 @@ All notable changes to this project will be documented in this file. See ## [1.6.18](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.6.17...@amplitude/plugin-session-replay-browser@1.6.18) (2024-08-13) -**Note:** Version bump only for package @amplitude/plugin-session-replay-browser - -# Change Log +### Bug Fixes -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. +- **session replay plugin:** remove unused if check + ([fc63646](https://github.com/amplitude/Amplitude-TypeScript/commit/fc6364683c416778b0609f20370454ee45437230)) +- **session replay:** rebase fixes + ([8386ede](https://github.com/amplitude/Amplitude-TypeScript/commit/8386ede0f8f61b71ed0b73d78967d465ed3567e9)) -## [1.6.17](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.6.16...@amplitude/plugin-session-replay-browser@1.6.17) (2024-08-12) +### Features -**Note:** Version bump only for package @amplitude/plugin-session-replay-browser +- **session replay plugin:** support targeting by user properties + ([ba8e27d](https://github.com/amplitude/Amplitude-TypeScript/commit/ba8e27d070b2015afc846f7ef02b745cff485d76)) +- **session replay:** add ability to target by single event trigger + ([dab74f7](https://github.com/amplitude/Amplitude-TypeScript/commit/dab74f73dd5946ac99e517c57d97819acf3677f4)) +- **session replay:** update method names, and ensure targeting is independent from sampling + ([6d3a7be](https://github.com/amplitude/Amplitude-TypeScript/commit/6d3a7be4169e6e0e3bd8c7103895374daea107bd)) # Change Log diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 39c58f48d..e495b6f23 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,10 @@ { "name": "@amplitude/plugin-session-replay-browser", +<<<<<<< HEAD "version": "1.6.22", +======= + "version": "1.7.0-srtargeting.0", +>>>>>>> 272189c9 (feat(session replay): add ability to capture replays based on targeting via remote config) "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +45,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^1.13.6", + "@amplitude/session-replay-browser": "^1.14.0-srtargeting.0", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index e69de29bb..2eba98982 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -0,0 +1,11 @@ +import { IdentifyOperation } from '@amplitude/analytics-types'; + +export const PROPERTY_ADD_OPERATIONS = [ + IdentifyOperation.SET, + IdentifyOperation.SET_ONCE, + IdentifyOperation.ADD, + IdentifyOperation.APPEND, + IdentifyOperation.PREPEND, + IdentifyOperation.POSTINSERT, + IdentifyOperation.POSTINSERT, +]; diff --git a/packages/plugin-session-replay-browser/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts new file mode 100644 index 000000000..53b9c6643 --- /dev/null +++ b/packages/plugin-session-replay-browser/src/helpers.ts @@ -0,0 +1,22 @@ +import { Event, IdentifyOperation } from '@amplitude/analytics-types'; +import { PROPERTY_ADD_OPERATIONS } from './constants'; + +export const parseUserProperties = (event: Event) => { + if (!event.user_properties) { + return; + } + let userPropertiesObj = {}; + const userPropertyKeys = Object.keys(event.user_properties); + + userPropertyKeys.forEach((identifyKey) => { + if (PROPERTY_ADD_OPERATIONS.includes(identifyKey as IdentifyOperation)) { + const typedUserPropertiesOperation = + event.user_properties && (event.user_properties[identifyKey as IdentifyOperation] as Record); + userPropertiesObj = { + ...userPropertiesObj, + ...typedUserPropertiesOperation, + }; + } + }); + return userPropertiesObj; +}; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index be5a45918..396134690 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,5 +1,7 @@ -import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types'; +import { getAnalyticsConnector } from '@amplitude/analytics-client-common'; +import { BrowserConfig, EnrichmentPlugin, Event, SpecialEventType } from '@amplitude/analytics-types'; import * as sessionReplay from '@amplitude/session-replay-browser'; +import { parseUserProperties } from './helpers'; import { SessionReplayOptions } from './typings/session-replay'; import { VERSION } from './version'; @@ -43,6 +45,9 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } } + const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore; + const userProperties = identityStore.getIdentity().userProperties; + await sessionReplay.init(config.apiKey, { instanceName: this.config.instanceName, deviceId: this.config.deviceId, @@ -63,6 +68,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { configEndpointUrl: this.options.configEndpointUrl, shouldInlineStylesheet: this.options.shouldInlineStylesheet, version: { type: 'plugin', version: VERSION }, + userProperties: userProperties, }).promise; } @@ -71,11 +77,20 @@ export class SessionReplayPlugin implements EnrichmentPlugin { // Choosing not to read from event object here, concerned about offline/delayed events messing up the state stored // in SR. if (this.config.sessionId && this.config.sessionId !== sessionReplay.getSessionId()) { - await sessionReplay.setSessionId(this.config.sessionId).promise; + const identityStore = getAnalyticsConnector(this.config.instanceName).identityStore; + const userProperties = identityStore.getIdentity().userProperties; + 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 // event is not of the current session (offline/late events). In that case, don't tag the events if (this.config.sessionId && this.config.sessionId === event.session_id) { + let userProperties; + if (event.event_type === SpecialEventType.IDENTIFY) { + userProperties = parseUserProperties(event); + } + await sessionReplay.evaluateTargetingAndCapture({ event, userProperties }); const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); event.event_properties = { ...event.event_properties, diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts index 8e7667a3b..c2c907c27 100644 --- a/packages/plugin-session-replay-browser/src/version.ts +++ b/packages/plugin-session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.6.22'; +export const VERSION = '1.7.0-srtargeting.0'; diff --git a/packages/plugin-session-replay-browser/test/helpers.test.ts b/packages/plugin-session-replay-browser/test/helpers.test.ts new file mode 100644 index 000000000..90b2eff61 --- /dev/null +++ b/packages/plugin-session-replay-browser/test/helpers.test.ts @@ -0,0 +1,40 @@ +import { SpecialEventType } from '@amplitude/analytics-types'; +import { parseUserProperties } from '../src/helpers'; + +describe('helpers', () => { + test('should return undefined if no user properties', () => { + const userProperties = parseUserProperties({ + event_type: SpecialEventType.IDENTIFY, + session_id: 123, + }); + expect(userProperties).toEqual(undefined); + }); + + test('should parse properties from their operation', () => { + const userProperties = parseUserProperties({ + event_type: SpecialEventType.IDENTIFY, + user_properties: { + $set: { + plan_id: 'free', + }, + }, + session_id: 123, + }); + expect(userProperties).toEqual({ + plan_id: 'free', + }); + }); + + test('should return an empty object if operations are not additive', () => { + const userProperties = parseUserProperties({ + event_type: SpecialEventType.IDENTIFY, + user_properties: { + $remove: { + plan_id: 'free', + }, + }, + session_id: 123, + }); + expect(userProperties).toEqual({}); + }); +}); diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index f2b2942fb..9a09fdc2d 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,7 +1,8 @@ -import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin } from '@amplitude/analytics-types'; +import { BrowserClient, BrowserConfig, LogLevel, Logger, Plugin, SpecialEventType } from '@amplitude/analytics-types'; import * as sessionReplayBrowser from '@amplitude/session-replay-browser'; import { SessionReplayPlugin, sessionReplayPlugin } from '../src/session-replay'; import { VERSION } from '../src/version'; +import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; jest.mock('@amplitude/session-replay-browser'); type MockedSessionReplayBrowser = jest.Mocked; @@ -11,7 +12,7 @@ type MockedLogger = jest.Mocked; type MockedBrowserClient = jest.Mocked; describe('SessionReplayPlugin', () => { - const { init, setSessionId, getSessionReplayProperties, shutdown, getSessionId } = + const { init, setSessionId, getSessionReplayProperties, evaluateTargetingAndCapture, shutdown, getSessionId } = sessionReplayBrowser as MockedSessionReplayBrowser; const mockLoggerProvider: MockedLogger = { error: jest.fn(), @@ -93,6 +94,30 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.config.flushIntervalMillis).toBe(0); }); + test('should pass user properties to plugin', async () => { + const updatedConfig: BrowserConfig = { ...mockConfig, sessionId: 456, instanceName: 'browser-sdk' }; + + const mockUserProperties = { + plan_id: 'free', + }; + jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({ + identityStore: { + getIdentity: () => { + return { + userProperties: mockUserProperties, + }; + }, + }, + } as unknown as ReturnType); + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(updatedConfig); + expect(init).toHaveBeenCalledTimes(1); + expect(init).toHaveBeenCalledWith( + mockConfig.apiKey, + expect.objectContaining({ userProperties: mockUserProperties }), + ); + }); + describe('defaultTracking', () => { test('should not change defaultTracking if its set to true', async () => { const sessionReplay = new SessionReplayPlugin(); @@ -182,6 +207,7 @@ describe('SessionReplayPlugin', () => { type: 'plugin', version: VERSION, }, + userProperties: {}, }); }); }); @@ -210,6 +236,55 @@ describe('SessionReplayPlugin', () => { }); }); + test('should evaluate targeting, passing the event', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup(mockConfig, mockAmplitude); + getSessionReplayProperties.mockReturnValueOnce({ + '[Amplitude] Session Replay ID': '123', + }); + const event = { + event_type: 'event_type', + event_properties: { + property_a: true, + property_b: 123, + }, + session_id: 123, + }; + + await sessionReplay.execute(event); + + expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({ + event: event, + userProperties: undefined, + }); + }); + + test('should parse user properties for identify event', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup(mockConfig, mockAmplitude); + getSessionReplayProperties.mockReturnValueOnce({ + '[Amplitude] Session Replay ID': '123', + }); + const event = { + event_type: SpecialEventType.IDENTIFY, + user_properties: { + $set: { + plan_id: 'free', + }, + }, + session_id: 123, + }; + + await sessionReplay.execute(event); + + expect(evaluateTargetingAndCapture).toHaveBeenCalledWith({ + event: event, + userProperties: { + plan_id: 'free', + }, + }); + }); + test('should not add event property for for event with mismatching session id.', async () => { const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup({ ...mockConfig }); @@ -242,7 +317,55 @@ describe('SessionReplayPlugin', () => { sessionReplay.config.sessionId = 456; await sessionReplay.execute(newEvent); expect(setSessionId).toHaveBeenCalledTimes(1); - expect(setSessionId).toHaveBeenCalledWith(456); + expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} }); + }); + + test('should update the session id on any event and pass along user properties', async () => { + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + + const event = { + event_type: 'session_start', + session_id: 456, + }; + const mockUserProperties = { + plan_id: 'free', + }; + jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({ + identityStore: { + getIdentity: () => { + return { + userProperties: mockUserProperties, + }; + }, + }, + } as unknown as ReturnType); + sessionReplay.config.sessionId = 456; + await sessionReplay.execute(event); + expect(setSessionId).toHaveBeenCalledTimes(1); + expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: mockUserProperties }); + }); + test('should update the session id on any event and pass along empty obj for user properties', async () => { + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + + const event = { + event_type: 'session_start', + session_id: 456, + }; + jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({ + identityStore: { + getIdentity: () => { + return { + userProperties: undefined, + }; + }, + }, + } as unknown as ReturnType); + sessionReplay.config.sessionId = 456; + await sessionReplay.execute(event); + expect(setSessionId).toHaveBeenCalledTimes(1); + expect(setSessionId).toHaveBeenCalledWith(456, '1a2b3c', { userProperties: {} }); }); test('should not update if session id unchanged', async () => { diff --git a/packages/session-replay-browser/README.md b/packages/session-replay-browser/README.md index a72bf5687..1e22f2965 100644 --- a/packages/session-replay-browser/README.md +++ b/packages/session-replay-browser/README.md @@ -47,8 +47,18 @@ sessionReplay.init(API_KEY, { }); ``` -### 3. Get session replay event properties -Any event that occurs within the span of a session replay 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 +### 3. Evaluate targeting (optional) +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. +```typescript +const sessionTargetingMatch = sessionReplay.evaluateTargetingAndCapture({ event: { + event_type: EVENT_NAME, + time: EVENT_TIMESTAMP, + event_properties: eventProperties +} }); +``` + +### 4. Get session replay event properties +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. ```typescript const sessionReplayProperties = sessionReplay.getSessionReplayProperties(); track(EVENT_NAME, { @@ -57,7 +67,7 @@ track(EVENT_NAME, { }) ``` -### 4. Update session id +### 5. Update session id 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: ```typescript sessionReplay.setSessionId(UNIX_TIMESTAMP) @@ -67,7 +77,7 @@ You can optionally pass a new device id as a second argument as well: sessionReplay.setSessionId(UNIX_TIMESTAMP, deviceId) ``` -### 5. Shutdown (optional) +### 6. Shutdown (optional) 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. ```typescript sessionReplay.shutdown() diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 7afb956cf..71d26a1f2 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "1.13.6", + "version": "1.14.0-srtargeting.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -43,6 +43,7 @@ "@amplitude/analytics-remote-config": "^0.4.0", "@amplitude/analytics-types": ">=1 <3", "@amplitude/rrweb": "2.0.0-alpha.20", + "@amplitude/targeting": "0.2.0", "idb": "^8.0.0", "tslib": "^2.4.1" }, diff --git a/packages/session-replay-browser/src/config/joined-config.ts b/packages/session-replay-browser/src/config/joined-config.ts index 0c878489d..186ae9329 100644 --- a/packages/session-replay-browser/src/config/joined-config.ts +++ b/packages/session-replay-browser/src/config/joined-config.ts @@ -78,6 +78,12 @@ export class SessionReplayJoinedConfigGenerator { sessionId, ); + const targetingConfig = await this.remoteConfigFetch.getRemoteConfig( + 'sessionReplay', + 'sr_targeting_config', + sessionId, + ); + // This is intentionally forced to only be set through the remote config. config.interactionConfig = await this.remoteConfigFetch.getRemoteConfig( 'sessionReplay', @@ -93,6 +99,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; @@ -104,7 +113,11 @@ export class SessionReplayJoinedConfigGenerator { return config; } - const { sr_sampling_config: samplingConfig, sr_privacy_config: remotePrivacyConfig } = remoteConfig; + const { + sr_sampling_config: samplingConfig, + sr_privacy_config: remotePrivacyConfig, + 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; @@ -188,6 +201,10 @@ export class SessionReplayJoinedConfigGenerator { ); } + if (targetingConfig && Object.keys(targetingConfig).length > 0) { + config.targetingConfig = targetingConfig; + } + this.localConfig.loggerProvider.debug( JSON.stringify({ name: 'session replay joined config', config: getDebugConfig(config) }, null, 2), ); diff --git a/packages/session-replay-browser/src/config/types.ts b/packages/session-replay-browser/src/config/types.ts index 8d731f442..10462384a 100644 --- a/packages/session-replay-browser/src/config/types.ts +++ b/packages/session-replay-browser/src/config/types.ts @@ -1,4 +1,5 @@ import { Config, LogLevel, Logger } from '@amplitude/analytics-types'; +import { TargetingFlag } from '@amplitude/targeting'; export interface SamplingConfig { sample_rate: number; @@ -10,11 +11,13 @@ export interface InteractionConfig { enabled: boolean; // defaults to false batch: boolean; // defaults to false } +export type TargetingConfig = TargetingFlag; export type SessionReplayRemoteConfig = { sr_sampling_config?: SamplingConfig; sr_privacy_config?: PrivacyConfig; sr_interaction_config?: InteractionConfig; + sr_targeting_config?: TargetingConfig; }; export interface SessionReplayRemoteConfigAPIResponse { @@ -49,11 +52,13 @@ export interface SessionReplayLocalConfig extends Config { configEndpointUrl?: string; shouldInlineStylesheet?: boolean; version?: SessionReplayVersion; + userProperties?: { [key: string]: any }; } export interface SessionReplayJoinedConfig extends SessionReplayLocalConfig { captureEnabled?: boolean; interactionConfig?: InteractionConfig; + targetingConfig?: TargetingConfig; } export interface SessionReplayRemoteConfigFetch { diff --git a/packages/session-replay-browser/src/index.ts b/packages/session-replay-browser/src/index.ts index 603fa24d1..807a922ed 100644 --- a/packages/session-replay-browser/src/index.ts +++ b/packages/session-replay-browser/src/index.ts @@ -1,3 +1,11 @@ import sessionReplay from './session-replay-factory'; -export const { init, setSessionId, getSessionId, getSessionReplayProperties, flush, shutdown } = sessionReplay; +export const { + init, + setSessionId, + getSessionId, + evaluateTargetingAndCapture, + getSessionReplayProperties, + flush, + shutdown, +} = sessionReplay; export { SessionReplayOptions } from './typings/session-replay'; diff --git a/packages/session-replay-browser/src/session-replay-factory.ts b/packages/session-replay-browser/src/session-replay-factory.ts index 7dfb736bf..ee7c99f60 100644 --- a/packages/session-replay-browser/src/session-replay-factory.ts +++ b/packages/session-replay-browser/src/session-replay-factory.ts @@ -17,16 +17,17 @@ const createInstance: () => AmplitudeSessionReplay = () => { const sessionReplay = new SessionReplay(); return { init: debugWrapper(sessionReplay.init.bind(sessionReplay), 'init', getLogConfig(sessionReplay)), + evaluateTargetingAndCapture: debugWrapper( + sessionReplay.evaluateTargetingAndCapture.bind(sessionReplay), + 'evaluateTargetingAndRecord', + getLogConfig(sessionReplay), + ), setSessionId: debugWrapper( sessionReplay.setSessionId.bind(sessionReplay), 'setSessionId', getLogConfig(sessionReplay), ), - getSessionId: debugWrapper( - sessionReplay.getSessionId.bind(sessionReplay), - 'getSessionId', - getLogConfig(sessionReplay), - ), + getSessionId: sessionReplay.getSessionId.bind(sessionReplay), getSessionReplayProperties: debugWrapper( sessionReplay.getSessionReplayProperties.bind(sessionReplay), 'getSessionReplayProperties', diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 06d8a2912..f9d446cec 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -1,8 +1,9 @@ import { getAnalyticsConnector, getGlobalScope } from '@amplitude/analytics-client-common'; import { Logger, returnWrapper } from '@amplitude/analytics-core'; -import { Logger as ILogger, LogLevel } from '@amplitude/analytics-types'; +import { Logger as ILogger, LogLevel, SpecialEventType } from '@amplitude/analytics-types'; import { pack, record } from '@amplitude/rrweb'; import { scrollCallback } from '@amplitude/rrweb-types'; +import { TargetingParameters } from '@amplitude/targeting'; import { createSessionReplayJoinedConfigGenerator } from './config/joined-config'; import { SessionReplayJoinedConfig, SessionReplayJoinedConfigGenerator } from './config/types'; import { @@ -20,6 +21,7 @@ import { generateHashCode, getDebugConfig, getStorageSize, isSessionInSample, ma import { clickBatcher, clickHook, clickNonBatcher } from './hooks/click'; import { ScrollWatcher } from './hooks/scroll'; import { SessionIdentifiers } from './identifiers'; +import { evaluateTargetingAndStore } from './targeting/targeting-manager'; import { AmplitudeSessionReplay, SessionReplayEventsManager as AmplitudeSessionReplayEventsManager, @@ -42,6 +44,7 @@ export class SessionReplay implements AmplitudeSessionReplay { loggerProvider: ILogger; recordCancelCallback: ReturnType | null = null; eventCount = 0; + sessionTargetingMatch = false; // Visible for testing pageLeaveFns: PageLeaveFn[] = []; @@ -119,18 +122,27 @@ export class SessionReplay implements AmplitudeSessionReplay { this.eventsManager = new MultiEventManager<'replay' | 'interaction', string>(...managers); + this.identifiers.deviceId && + void this.eventsManager.sendStoredEvents({ + deviceId: this.identifiers.deviceId, + }); + this.loggerProvider.log('Installing @amplitude/session-replay-browser.'); this.teardownEventListeners(false); - this.initialize(true); + this.stopRecordingEvents(); + await this.evaluateTargetingAndCapture({ userProperties: options.userProperties }); } - setSessionId(sessionId: number, deviceId?: string) { - return returnWrapper(this.asyncSetSessionId(sessionId, deviceId)); + setSessionId(sessionId: number, deviceId?: string, options?: { userProperties?: { [key: string]: any } }) { + return returnWrapper(this.asyncSetSessionId(sessionId, deviceId, options)); } - async asyncSetSessionId(sessionId: number, deviceId?: string) { + async asyncSetSessionId(sessionId: number, deviceId?: string, options?: { userProperties?: { [key: string]: any } }) { + this.stopRecordingEvents(); + this.sessionTargetingMatch = false; + const previousSessionId = this.identifiers && this.identifiers.sessionId; if (previousSessionId) { this.sendEvents(previousSessionId); @@ -147,7 +159,7 @@ export class SessionReplay implements AmplitudeSessionReplay { if (this.joinedConfigGenerator && previousSessionId) { this.config = await this.joinedConfigGenerator.generateJoinedConfig(this.identifiers.sessionId); } - this.recordEvents(); + await this.evaluateTargetingAndCapture({ userProperties: options?.userProperties }); } getSessionReplayDebugPropertyValue() { @@ -166,7 +178,7 @@ export class SessionReplay implements AmplitudeSessionReplay { return {}; } - const shouldRecord = this.getShouldRecord(); + const shouldRecord = this.getShouldCapture(); let eventProperties: { [key: string]: string | null } = {}; if (shouldRecord) { @@ -201,7 +213,8 @@ export class SessionReplay implements AmplitudeSessionReplay { focusListener = () => { // Restart recording on focus to ensure that when user // switches tabs, we take a full snapshot - this.recordEvents(); + this.stopRecordingEvents(); + this.captureEvents(); }; /** @@ -215,6 +228,40 @@ export class SessionReplay implements AmplitudeSessionReplay { }); }; + evaluateTargetingAndCapture = async (targetingParams?: Pick) => { + if (!this.identifiers || !this.identifiers.sessionId || !this.config) { + if (this.identifiers && !this.identifiers.sessionId) { + this.loggerProvider.log('Session ID has not been set yet, cannot evaluate targeting for Session Replay.'); + } else { + this.loggerProvider.warn('Session replay init has not been called, cannot evaluate targeting.'); + } + return; + } + + if (this.config.targetingConfig && !this.sessionTargetingMatch) { + let eventForTargeting = targetingParams?.event; + if ( + eventForTargeting && + Object.values(SpecialEventType).includes(eventForTargeting.event_type as SpecialEventType) + ) { + eventForTargeting = undefined; + } + + // We're setting this on this class because fetching the value from idb + // is async, we need to access this value synchronously (for record + // and for getSessionReplayProperties - both synchronous fns) + this.sessionTargetingMatch = await evaluateTargetingAndStore({ + sessionId: this.identifiers.sessionId, + targetingConfig: this.config.targetingConfig, + loggerProvider: this.loggerProvider, + apiKey: this.config.apiKey, + targetingParams: { userProperties: targetingParams?.userProperties, event: eventForTargeting }, + }); + } + + this.captureEvents(); + }; + sendEvents(sessionId?: number) { const sessionIdToSend = sessionId || this.identifiers?.sessionId; const deviceId = this.getDeviceId(); @@ -224,22 +271,6 @@ export class SessionReplay implements AmplitudeSessionReplay { this.eventsManager.sendCurrentSequenceEvents({ sessionId: sessionIdToSend, deviceId }); } - initialize(shouldSendStoredEvents = false) { - if (!this.identifiers?.sessionId) { - this.loggerProvider.log(`Session is not being recorded due to lack of session id.`); - return; - } - - const deviceId = this.getDeviceId(); - if (!deviceId) { - this.loggerProvider.log(`Session is not being recorded due to lack of device id.`); - return; - } - this.eventsManager && shouldSendStoredEvents && this.eventsManager.sendStoredEvents({ deviceId }); - - this.recordEvents(); - } - shouldOptOut() { let identityStoreOptOut: boolean | undefined; if (this.config?.instanceName) { @@ -250,9 +281,9 @@ export class SessionReplay implements AmplitudeSessionReplay { return identityStoreOptOut !== undefined ? identityStoreOptOut : this.config?.optOut; } - getShouldRecord() { + getShouldCapture() { if (!this.identifiers || !this.config || !this.identifiers.sessionId) { - this.loggerProvider.warn(`Session is not being recorded due to lack of config, please call sessionReplay.init.`); + this.loggerProvider.warn(`Session is not being captured due to lack of config, please call sessionReplay.init.`); return false; } if (!this.config.captureEnabled) { @@ -263,15 +294,38 @@ export class SessionReplay implements AmplitudeSessionReplay { } if (this.shouldOptOut()) { - this.loggerProvider.log(`Opting session ${this.identifiers.sessionId} out of recording due to optOut config.`); + this.loggerProvider.log( + `Opting session ${this.identifiers.sessionId} out of replay capture due to optOut config.`, + ); return false; } - const isInSample = isSessionInSample(this.identifiers.sessionId, this.config.sampleRate); - if (!isInSample) { - this.loggerProvider.log(`Opting session ${this.identifiers.sessionId} out of recording due to sample rate.`); + // If targetingConfig exists, we'll use the sessionTargetingMatch to determine whether to record + // Otherwise, we'll evaluate the session against the overall sample rate + if (this.config.targetingConfig) { + if (!this.sessionTargetingMatch) { + this.loggerProvider.log( + `Not capturing replays for session ${this.identifiers.sessionId} due to not matching targeting conditions.`, + ); + return false; + } + this.loggerProvider.log( + `Capturing replays for session ${this.identifiers.sessionId} due to matching targeting conditions.`, + ); + } else { + const isInSample = isSessionInSample(this.identifiers.sessionId, this.config.sampleRate); + if (!isInSample) { + this.loggerProvider.log( + `Opting session ${this.identifiers.sessionId} out of replay capture due to sample rate.`, + ); + return false; + } + this.loggerProvider.log( + `Capturing replays for session ${this.identifiers.sessionId} due to inclusion in sample rate.`, + ); } - return isInSample; + + return true; } getBlockSelectors(): string | string[] | undefined { @@ -298,20 +352,25 @@ export class SessionReplay implements AmplitudeSessionReplay { return maskSelector as unknown as string; } - recordEvents() { - const shouldRecord = this.getShouldRecord(); - const sessionId = this.identifiers?.sessionId; + captureEvents() { + if (this.recordCancelCallback) { + this.loggerProvider.debug('captureEvents method fired - Session Replay capture already in progress.'); + return; + } + + const shouldRecord = this.getShouldCapture(); + const sessionId = this.identifiers && this.identifiers.sessionId; if (!shouldRecord || !sessionId || !this.config) { return; } - this.stopRecordingEvents(); + const privacyConfig = this.config.privacyConfig; this.loggerProvider.log('Session Replay capture beginning.'); this.recordCancelCallback = record({ emit: (event) => { if (this.shouldOptOut()) { - this.loggerProvider.log(`Opting session ${sessionId} out of recording due to optOut config.`); + this.loggerProvider.log(`Opting session ${sessionId} out of replay capture due to optOut config.`); this.stopRecordingEvents(); this.sendEvents(); return; @@ -406,7 +465,9 @@ export class SessionReplay implements AmplitudeSessionReplay { stopRecordingEvents = () => { try { - this.loggerProvider.log('Session Replay capture stopping.'); + if (this.recordCancelCallback) { + this.loggerProvider.log('Session Replay capture stopping.'); + } this.recordCancelCallback && this.recordCancelCallback(); this.recordCancelCallback = null; } catch (error) { diff --git a/packages/session-replay-browser/src/targeting/targeting-idb-store.ts b/packages/session-replay-browser/src/targeting/targeting-idb-store.ts new file mode 100644 index 000000000..920222462 --- /dev/null +++ b/packages/session-replay-browser/src/targeting/targeting-idb-store.ts @@ -0,0 +1,111 @@ +import { Logger as ILogger } from '@amplitude/analytics-types'; +import { DBSchema, IDBPDatabase, openDB } from 'idb'; + +export const MAX_IDB_STORAGE_LENGTH = 1000 * 60 * 60 * 24 * 2; // 2 days +export interface SessionReplayTargetingDB extends DBSchema { + sessionTargetingMatch: { + key: number; + value: { + sessionId: number; + targetingMatch: boolean; + }; + }; +} + +export class TargetingIDBStore { + dbs: { [apiKey: string]: IDBPDatabase } = {}; + + createStore = async (dbName: string) => { + return await openDB(dbName, 1, { + upgrade: (db: IDBPDatabase) => { + if (!db.objectStoreNames.contains('sessionTargetingMatch')) { + db.createObjectStore('sessionTargetingMatch', { + keyPath: 'sessionId', + }); + } + }, + }); + }; + + openOrCreateDB = async (apiKey: string) => { + if (this.dbs && this.dbs[apiKey]) { + return this.dbs[apiKey]; + } + const dbName = `${apiKey.substring(0, 10)}_amp_session_replay_targeting`; + const db = await this.createStore(dbName); + this.dbs[apiKey] = db; + return db; + }; + + getTargetingMatchForSession = async ({ + loggerProvider, + apiKey, + sessionId, + }: { + loggerProvider: ILogger; + apiKey: string; + sessionId: number; + }) => { + try { + const db = await this.openOrCreateDB(apiKey); + const targetingMatchForSession = await db.get<'sessionTargetingMatch'>('sessionTargetingMatch', sessionId); + + return targetingMatchForSession?.targetingMatch; + } catch (e) { + loggerProvider.warn(`Failed to get targeting match for session id ${sessionId}: ${e as string}`); + } + return undefined; + }; + + storeTargetingMatchForSession = async ({ + loggerProvider, + apiKey, + sessionId, + targetingMatch, + }: { + loggerProvider: ILogger; + apiKey: string; + sessionId: number; + targetingMatch: boolean; + }) => { + try { + const db = await this.openOrCreateDB(apiKey); + const targetingMatchForSession = await db.put<'sessionTargetingMatch'>('sessionTargetingMatch', { + targetingMatch, + sessionId, + }); + + return targetingMatchForSession; + } catch (e) { + loggerProvider.warn(`Failed to store targeting match for session id ${sessionId}: ${e as string}`); + } + return undefined; + }; + + clearStoreOfOldSessions = async ({ + loggerProvider, + apiKey, + currentSessionId, + }: { + loggerProvider: ILogger; + apiKey: string; + currentSessionId: number; + }) => { + try { + const db = await this.openOrCreateDB(apiKey); + const tx = db.transaction<'sessionTargetingMatch', 'readwrite'>('sessionTargetingMatch', 'readwrite'); + const allTargetingMatchObjs = await tx.store.getAll(); + for (let i = 0; i < allTargetingMatchObjs.length; i++) { + const targetingMatchObj = allTargetingMatchObjs[i]; + const amountOfTimeSinceSession = Date.now() - targetingMatchObj.sessionId; + if (targetingMatchObj.sessionId !== currentSessionId && amountOfTimeSinceSession > MAX_IDB_STORAGE_LENGTH) { + await tx.store.delete(targetingMatchObj.sessionId); + } + } + await tx.done; + } catch (e) { + loggerProvider.warn(`Failed to clear old targeting matches for sessions: ${e as string}`); + } + }; +} +export const targetingIDBStore = new TargetingIDBStore(); diff --git a/packages/session-replay-browser/src/targeting/targeting-manager.ts b/packages/session-replay-browser/src/targeting/targeting-manager.ts new file mode 100644 index 000000000..f68a068a5 --- /dev/null +++ b/packages/session-replay-browser/src/targeting/targeting-manager.ts @@ -0,0 +1,61 @@ +import { TargetingParameters, evaluateTargeting as evaluateTargetingPackage } from '@amplitude/targeting'; +import { TargetingConfig } from '../config/types'; +import { Logger } from '@amplitude/analytics-types'; +import { targetingIDBStore } from './targeting-idb-store'; + +export const evaluateTargetingAndStore = async ({ + sessionId, + targetingConfig, + loggerProvider, + apiKey, + targetingParams, +}: { + sessionId: number; + targetingConfig: TargetingConfig; + loggerProvider: Logger; + apiKey: string; + targetingParams?: Pick; +}) => { + await targetingIDBStore.clearStoreOfOldSessions({ + loggerProvider: loggerProvider, + apiKey: apiKey, + currentSessionId: sessionId, + }); + + const idbTargetingMatch = await targetingIDBStore.getTargetingMatchForSession({ + loggerProvider: loggerProvider, + apiKey: apiKey, + sessionId: sessionId, + }); + if (idbTargetingMatch === true) { + return true; + } + + // 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 + let sessionTargetingMatch = true; + try { + const targetingResult = await evaluateTargetingPackage({ + ...targetingParams, + flag: targetingConfig, + sessionId: sessionId, + apiKey: apiKey, + loggerProvider: loggerProvider, + }); + if (targetingResult && targetingResult.sr_targeting_config) { + sessionTargetingMatch = targetingResult.sr_targeting_config.key === 'on'; + } + + void targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: loggerProvider, + apiKey: apiKey, + sessionId: sessionId, + targetingMatch: sessionTargetingMatch, + }); + } catch (err: unknown) { + const knownError = err as Error; + loggerProvider.warn(knownError.message); + } + return sessionTargetingMatch; +}; diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index 55e4fe023..cf447577e 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -1,5 +1,6 @@ import { AmplitudeReturn, ServerZone } from '@amplitude/analytics-types'; import { SessionReplayJoinedConfig, SessionReplayLocalConfig, SessionReplayVersion } from '../config/types'; +import { TargetingParameters } from '@amplitude/targeting'; export type StorageData = { totalStorageSize: number; @@ -71,9 +72,16 @@ export type SessionReplayOptions = Omit AmplitudeReturn; - setSessionId: (sessionId: number, deviceId?: string) => AmplitudeReturn; + setSessionId: ( + sessionId: number, + deviceId?: string, + options?: { userProperties?: { [key: string]: any } }, + ) => AmplitudeReturn; getSessionId: () => number | undefined; getSessionReplayProperties: () => { [key: string]: boolean | string | null }; + evaluateTargetingAndCapture: ( + targetingParams?: Pick, + ) => Promise; flush: (useRetry: boolean) => Promise; shutdown: () => void; } diff --git a/packages/session-replay-browser/src/version.ts b/packages/session-replay-browser/src/version.ts index 2d422325d..307a8abeb 100644 --- a/packages/session-replay-browser/src/version.ts +++ b/packages/session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.13.6'; +export const VERSION = '1.14.0-srtargeting.0'; diff --git a/packages/session-replay-browser/test/flag-config-data.ts b/packages/session-replay-browser/test/flag-config-data.ts new file mode 100644 index 000000000..f783335a6 --- /dev/null +++ b/packages/session-replay-browser/test/flag-config-data.ts @@ -0,0 +1,84 @@ +export const flagConfig = { + key: 'sr_targeting_config', + variants: { + on: { key: 'on' }, + off: { key: 'off' }, + }, + segments: [ + { + metadata: { segmentName: 'sign in trigger' }, + bucket: { + selector: ['context', 'session_id'], + salt: 'xdfrewd', + allocations: [ + { + range: [0, 99], + distributions: [ + { + variant: 'on', + range: [0, 42949673], + }, + ], + }, + ], + }, + conditions: [ + [ + { + selector: ['context', 'event', 'event_type'], + op: 'is', + values: ['Sign In'], + }, + ], + ], + }, + { + metadata: { segmentName: 'user property' }, + bucket: { + selector: ['context', 'session_id'], + salt: 'Rpr5h4vy', + allocations: [ + { + range: [0, 99], + distributions: [ + { + variant: 'on', + range: [0, 42949673], + }, + ], + }, + ], + }, + conditions: [ + [ + { + selector: ['context', 'user', 'user_properties', 'country'], + op: 'set contains any', + values: ['united states'], + }, + ], + ], + }, + { + metadata: { segmentName: 'leftover allocation' }, + bucket: { + selector: ['context', 'session_id'], + salt: 'T5lhyRo', + allocations: [ + { + range: [0, 9], // Selects 10% of users that match these conditions + distributions: [ + { + variant: 'on', + range: [0, 42949673], + }, + ], + }, + ], + }, + }, + { + variant: 'off', + }, + ], +}; diff --git a/packages/session-replay-browser/test/integration/sampling.test.ts b/packages/session-replay-browser/test/integration/sampling.test.ts index cde469dbc..e2d2f475c 100644 --- a/packages/session-replay-browser/test/integration/sampling.test.ts +++ b/packages/session-replay-browser/test/integration/sampling.test.ts @@ -13,6 +13,8 @@ import { DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_SE import * as Helpers from '../../src/helpers'; import { SessionReplay } from '../../src/session-replay'; import { SESSION_ID_IN_20_SAMPLE } from '../test-data'; +import { SessionReplayRemoteConfig } from '../../src/config/types'; +import { flagConfig } from '../flag-config-data'; type MockedLogger = jest.Mocked; jest.mock('@amplitude/rrweb'); @@ -159,9 +161,14 @@ describe('module level integration', () => { expect(inSampleSpy).toHaveBeenCalledWith(sessionReplay.identifiers?.sessionId, 0.8); }); }); - describe('with remote config set', () => { + describe('with sampling config in remote config', () => { beforeEach(() => { - getRemoteConfigMock.mockResolvedValue(samplingConfig); + getRemoteConfigMock.mockImplementation((namespace: string, key: keyof SessionReplayRemoteConfig) => { + if (namespace === 'sessionReplay' && key === 'sr_sampling_config') { + return samplingConfig; + } + return; + }); }); test('should capture', async () => { const sessionReplay = new SessionReplay(); @@ -197,6 +204,66 @@ describe('module level integration', () => { expect(inSampleSpy).toHaveBeenCalledWith(sessionReplay.identifiers?.sessionId, 0.5); }); }); + describe('with sampling config and targeting config in remote config', () => { + beforeEach(() => { + getRemoteConfigMock.mockImplementation((namespace: string, key: keyof SessionReplayRemoteConfig) => { + if (namespace === 'sessionReplay' && key === 'sr_sampling_config') { + return samplingConfig; + } + if (namespace === 'sessionReplay' && key === 'sr_targeting_config') { + return flagConfig; + } + return; + }); + }); + test('should not capture if no targeting match', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions }).promise; + const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); + const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] + .value; + + jest.spyOn(createEventsIDBStoreInstance, 'storeCurrentSequence'); + expect(sessionRecordingProperties).not.toMatchObject({ + [DEFAULT_SESSION_REPLAY_PROPERTY]: `1a2b3c/${SESSION_ID_IN_20_SAMPLE}`, + }); + expect(record).not.toHaveBeenCalled(); + }); + test('should capture if targeting match', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions }).promise; + await sessionReplay.evaluateTargetingAndCapture({ event: { event_type: 'Sign In' } }); + const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); + const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] + .value; + + jest.spyOn(createEventsIDBStoreInstance, 'storeCurrentSequence'); + expect(sessionRecordingProperties).toMatchObject({ + [DEFAULT_SESSION_REPLAY_PROPERTY]: `1a2b3c/${SESSION_ID_IN_20_SAMPLE}`, + }); + expect(record).toHaveBeenCalled(); + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + sessionReplay.sendEvents(); + await (createEventsIDBStoreInstance.storeCurrentSequence as jest.Mock).mock.results[0].value; + await runScheduleTimers(); + expect(fetch).toHaveBeenLastCalledWith( + `${SESSION_REPLAY_SERVER_URL}?device_id=1a2b3c&session_id=${SESSION_ID_IN_20_SAMPLE}&seq_number=1&type=replay`, + expect.anything(), + ); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.log).toHaveBeenLastCalledWith( + 'Session replay event batch with seq id 1 tracked successfully for session id 1719847315000, size of events: 0 KB', + ); + }); + + test('should not use sampleRate', async () => { + const inSampleSpy = jest.spyOn(Helpers, 'isSessionInSample'); + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.8 }).promise; + expect(inSampleSpy).not.toHaveBeenCalled(); + }); + }); }); describe('sampling logic', () => { beforeEach(() => { diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 80877d856..7dadf4df4 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -9,18 +9,29 @@ import * as RRWeb from '@amplitude/rrweb'; import { SessionReplayLocalConfig } from '../src/config/local-config'; import { IDBFactory } from 'fake-indexeddb'; -import { InteractionConfig, SessionReplayJoinedConfig, SessionReplayRemoteConfig } from '../src/config/types'; +import { SessionReplayJoinedConfig, SessionReplayRemoteConfig } from '../src/config/types'; import { CustomRRwebEvent, DEFAULT_SAMPLE_RATE } from '../src/constants'; import * as SessionReplayIDB from '../src/events/events-idb-store'; import * as Helpers from '../src/helpers'; import { SessionReplay } from '../src/session-replay'; +import { SessionReplayTargetingDB, targetingIDBStore } from '../src/targeting/targeting-idb-store'; +import * as TargetingManager from '../src/targeting/targeting-manager'; import { SessionReplayOptions } from '../src/typings/session-replay'; +import * as JoinedConfigGenerator from '../src/config/joined-config'; +import { SessionReplayJoinedConfigGenerator } from '../src/config/joined-config'; +import { flagConfig } from './flag-config-data'; +import { IDBPDatabase } from 'idb'; jest.mock('@amplitude/rrweb'); type MockedRRWeb = jest.Mocked; type MockedLogger = jest.Mocked; +const samplingConfig = { + sample_rate: 1, + capture_enabled: true, +}; + const mockEvent = { type: 4, data: { href: 'https://analytics.amplitude.com/', width: 1728, height: 154 }, @@ -28,11 +39,6 @@ const mockEvent = { }; const mockEventString = JSON.stringify(mockEvent); -const samplingConfig = { - sample_rate: 1, - capture_enabled: true, -}; - describe('SessionReplay', () => { const { record } = RRWeb as MockedRRWeb; let originalFetch: typeof global.fetch; @@ -99,16 +105,23 @@ describe('SessionReplay', () => { }; let sessionReplay: SessionReplay; let getRemoteConfigMock: jest.Mock; - let initialize: jest.SpyInstance; + const evaluateTargetingAndStorePromise = Promise.resolve(true); + const mockConfig: SessionReplayJoinedConfig = { + ...new SessionReplayLocalConfig(apiKey, mockOptions), + optOut: false, + captureEnabled: true, + }; beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + record.mockReturnValue(() => {}); getRemoteConfigMock = jest.fn().mockResolvedValue(samplingConfig); jest.spyOn(RemoteConfigFetch, 'createRemoteConfigFetch').mockResolvedValue({ getRemoteConfig: getRemoteConfigMock, metrics: {}, }); + jest.spyOn(TargetingManager, 'evaluateTargetingAndStore').mockReturnValue(evaluateTargetingAndStorePromise); jest.spyOn(SessionReplayIDB, 'createEventsIDBStore'); sessionReplay = new SessionReplay(); - initialize = jest.spyOn(sessionReplay, 'initialize'); jest.useFakeTimers(); originalFetch = global.fetch; (global.fetch as jest.Mock) = jest.fn(() => { @@ -282,19 +295,12 @@ describe('SessionReplay', () => { sessionReplay.config && expectationFn(sessionReplay.config); }); - test('should call initialize with shouldSendStoredEvents=true', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - - expect(initialize).toHaveBeenCalledTimes(1); - - expect(initialize.mock.calls[0]).toEqual([true]); - }); test('should set up blur and focus event listeners', async () => { - const initialize = jest.spyOn(sessionReplay, 'initialize'); + const stopRecordingMock = jest.fn(); + sessionReplay.recordCancelCallback = stopRecordingMock; + const evaluateTargetingAndCapture = jest.spyOn(sessionReplay, 'evaluateTargetingAndCapture'); + sessionReplay.sessionTargetingMatch = true; await sessionReplay.init(apiKey, mockOptions).promise; - const recordMock = jest.fn(); - sessionReplay.recordEvents = recordMock; - initialize.mockReset(); expect(addEventListenerMock).toHaveBeenCalledTimes(3); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(addEventListenerMock.mock.calls[0][0]).toEqual('blur'); @@ -309,16 +315,19 @@ describe('SessionReplay', () => { const focusCallback = addEventListenerMock.mock.calls[1][1]; // eslint-disable-next-line @typescript-eslint/no-unsafe-call focusCallback(); - expect(recordMock).toHaveBeenCalled(); - }); - test('it should not call initialize if the document does not have focus', () => { - const initialize = jest.spyOn(sessionReplay, 'initialize'); - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - document: { - hasFocus: () => false, - }, - } as typeof globalThis); - expect(initialize).not.toHaveBeenCalled(); + expect(evaluateTargetingAndCapture).toHaveBeenCalled(); + }); + test('it should stop capture and restart if reinitialized', async () => { + const recordCancelCallbackMock = jest.fn(); + sessionReplay.recordCancelCallback = recordCancelCallbackMock; + // Mock the record method as if its returning a listener handle + // eslint-disable-next-line @typescript-eslint/no-empty-function + record.mockReturnValueOnce(() => {}); + await sessionReplay.init(apiKey, mockOptions).promise; + await sessionReplay.init(apiKey, mockOptions).promise; + expect(recordCancelCallbackMock).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledTimes(2); + // TODO - is there a way to check the number of mutation observers? }); describe('flushMaxRetries config', () => { @@ -346,17 +355,25 @@ describe('SessionReplay', () => { describe('setSessionId', () => { test('should stop recording events for current session', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - const stopRecordingMock = jest.fn(); + const recordCancelCallbackMock = jest.fn(); // Mock class as if it has already been recording events - sessionReplay.sendEvents = stopRecordingMock; + sessionReplay.recordCancelCallback = recordCancelCallbackMock; sessionReplay.setSessionId(456); - expect(stopRecordingMock).toHaveBeenCalled(); + expect(recordCancelCallbackMock).toHaveBeenCalled(); + }); + test('should set sessionTargetingMatch to false', async () => { + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.sessionTargetingMatch = true; + + sessionReplay.setSessionId(456); + expect(sessionReplay.sessionTargetingMatch).toBe(false); }); test('should update the session id and start recording', async () => { await sessionReplay.init(apiKey, mockOptions).promise; + console.log('resetting mock'); record.mockReset(); expect(sessionReplay.identifiers?.sessionId).toEqual(123); expect(sessionReplay.identifiers?.sessionReplayId).toEqual('1a2b3c/123'); @@ -372,7 +389,7 @@ describe('SessionReplay', () => { sessionReplay.setSessionId(456); expect(sessionReplay.identifiers?.sessionId).toEqual(456); expect(sessionReplay.identifiers?.sessionReplayId).toEqual('1a2b3c/456'); - return generateJoinedConfigPromise.then(() => { + return Promise.all([generateJoinedConfigPromise, evaluateTargetingAndStorePromise]).then(() => { expect(record).toHaveBeenCalledTimes(1); expect(sessionReplay.config).toEqual(updatedConfig); }); @@ -412,6 +429,16 @@ describe('SessionReplay', () => { expect(sessionReplay.identifiers?.deviceId).toEqual('9l8m7n'); expect(sessionReplay.getDeviceId()).toEqual('9l8m7n'); }); + + test('should pass userProperties to evaluateTargetingAndCapture', async () => { + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.loggerProvider = mockLoggerProvider; + const evaluateMock = jest.fn(); + sessionReplay.evaluateTargetingAndCapture = evaluateMock; + return sessionReplay.setSessionId(456, '9l8m7n', { userProperties: { plan_id: 'free' } }).promise.then(() => { + expect(evaluateMock).toHaveBeenCalledWith({ userProperties: { plan_id: 'free' } }); + }); + }); }); describe('getSessionId', () => { @@ -445,7 +472,7 @@ describe('SessionReplay', () => { test('should return an empty object if shouldRecord is false', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getShouldRecord = () => false; + sessionReplay.getShouldCapture = () => false; const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({}); @@ -453,7 +480,7 @@ describe('SessionReplay', () => { test('should return the session recorded property if shouldRecord is true', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getShouldRecord = () => true; + sessionReplay.getShouldCapture = () => true; const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({ @@ -469,6 +496,7 @@ describe('SessionReplay', () => { }, } as typeof globalThis); await sessionReplay.init(apiKey, { ...mockOptions, debugMode: true }).promise; + sessionReplay.sessionTargetingMatch = true; const result = sessionReplay.getSessionReplayProperties(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(result).toEqual({ @@ -479,7 +507,7 @@ describe('SessionReplay', () => { test('should return session replay id property with null', async () => { await sessionReplay.init(apiKey, { ...mockOptions }).promise; - sessionReplay.getShouldRecord = () => true; + sessionReplay.getShouldCapture = () => true; if (sessionReplay.identifiers) { sessionReplay.identifiers.sessionReplayId = undefined; } @@ -492,7 +520,7 @@ describe('SessionReplay', () => { test('should return debug property', async () => { await sessionReplay.init(apiKey, { ...mockOptions, debugMode: true }).promise; - sessionReplay.getShouldRecord = () => true; + sessionReplay.getShouldCapture = () => true; const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({ @@ -504,7 +532,7 @@ describe('SessionReplay', () => { test('should add a custom rrweb event', async () => { await sessionReplay.init(apiKey, { ...mockOptions, debugMode: true }).promise; sessionReplay.addCustomRRWebEvent = jest.fn(); - sessionReplay.getShouldRecord = () => true; + sessionReplay.getShouldCapture = () => true; const result = sessionReplay.getSessionReplayProperties(); expect(sessionReplay.addCustomRRWebEvent).toHaveBeenCalledWith( @@ -526,7 +554,7 @@ describe('SessionReplay', () => { test('should add a custom rrweb event with storage info if event count is 10, then reset event count', async () => { await sessionReplay.init(apiKey, { ...mockOptions, debugMode: true }).promise; sessionReplay.addCustomRRWebEvent = jest.fn(); - sessionReplay.getShouldRecord = () => true; + sessionReplay.getShouldCapture = () => true; sessionReplay.eventCount = 10; const result = sessionReplay.getSessionReplayProperties(); @@ -542,90 +570,6 @@ describe('SessionReplay', () => { }); }); - describe('initialize', () => { - test('should return early if session id not set', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - if (!sessionReplay.eventsManager || !sessionReplay.identifiers) { - throw new Error('Did not call init'); - } - sessionReplay.identifiers.sessionId = undefined; - const sendStoredEventsSpy = jest.spyOn(sessionReplay.eventsManager, 'sendStoredEvents'); - sessionReplay.initialize(); - expect(sendStoredEventsSpy).not.toHaveBeenCalled(); - }); - test('should return early if no identifiers', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.identifiers = undefined; - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const sendStoredEventsSpy = jest.spyOn(sessionReplay.eventsManager, 'sendStoredEvents'); - sessionReplay.initialize(); - expect(sendStoredEventsSpy).not.toHaveBeenCalled(); - }); - test('should return early if no device id', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getDeviceId = jest.fn().mockReturnValue(undefined); - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const sendStoredEventsSpy = jest.spyOn(sessionReplay.eventsManager, 'sendStoredEvents'); - sessionReplay.initialize(); - expect(sendStoredEventsSpy).not.toHaveBeenCalled(); - }); - test('should send stored events and record events', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - record.mockReset(); - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const eventsManagerInitSpy = jest.spyOn(sessionReplay.eventsManager, 'sendStoredEvents'); - - sessionReplay.initialize(true); - expect(eventsManagerInitSpy).toHaveBeenCalledWith({ - deviceId: mockOptions.deviceId, - }); - expect(record).toHaveBeenCalledTimes(1); - }); - test('should not send stored events if shouldSendStoredEvents is false', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - record.mockReset(); - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const eventsManagerInitSpy = jest.spyOn(sessionReplay.eventsManager, 'sendStoredEvents'); - - sessionReplay.initialize(false); - expect(eventsManagerInitSpy).not.toHaveBeenCalled(); - expect(record).toHaveBeenCalledTimes(1); - }); - - test.each([ - { enabled: true, expectedLength: 1 }, - { enabled: false, expectedLength: 0 }, - { enabled: undefined, expectedLength: 0 }, - ])('should not register scroll if interaction config not enabled', async ({ enabled, expectedLength }) => { - getRemoteConfigMock = jest.fn().mockImplementation((namespace: string, key: keyof SessionReplayRemoteConfig) => { - if (namespace === 'sessionReplay' && key === 'sr_interaction_config') { - return { - enabled, - } as InteractionConfig; - } - return; - }); - jest.spyOn(RemoteConfigFetch, 'createRemoteConfigFetch').mockResolvedValue({ - getRemoteConfig: getRemoteConfigMock, - metrics: {}, - }); - await sessionReplay.init(apiKey, { - ...mockOptions, - sampleRate: 0.5, - }).promise; - await sessionReplay.init(apiKey, { ...mockOptions }).promise; - expect(sessionReplay.pageLeaveFns).toHaveLength(expectedLength); - }); - }); - describe('shouldOptOut', () => { test('should return undefined if no config set', () => { expect(sessionReplay.shouldOptOut()).toEqual(undefined); @@ -662,21 +606,22 @@ describe('SessionReplay', () => { }); }); - describe('getShouldRecord', () => { + describe('getShouldCapture', () => { test('should return true if there are options', async () => { await sessionReplay.init(apiKey, mockOptions).promise; const sampleRate = sessionReplay.config?.sampleRate; expect(sampleRate).toBe(mockOptions.sampleRate); - const shouldRecord = sessionReplay.getShouldRecord(); + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(true); }); test('should return false if no options', async () => { // Mock as if remote config call fails getRemoteConfigMock.mockImplementation(() => Promise.reject('error')); await sessionReplay.init(apiKey, mockEmptyOptions).promise; + sessionReplay.sessionTargetingMatch = true; const sampleRate = sessionReplay.config?.sampleRate; expect(sampleRate).toBe(DEFAULT_SAMPLE_RATE); - const shouldRecord = sessionReplay.getShouldRecord(); + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('should return false if captureEnabled is false', async () => { @@ -686,7 +631,8 @@ describe('SessionReplay', () => { }); await sessionReplay.init(apiKey, { ...mockOptions }).promise; - const shouldRecord = sessionReplay.getShouldRecord(); + sessionReplay.sessionTargetingMatch = true; + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('should return false if session not included in sample rate', async () => { @@ -695,35 +641,46 @@ describe('SessionReplay', () => { jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => false); await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.2 }).promise; + sessionReplay.sessionTargetingMatch = true; const sampleRate = sessionReplay.config?.sampleRate; expect(sampleRate).toBe(0.2); - const shouldRecord = sessionReplay.getShouldRecord(); + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('should set record as true if session is included in sample rate', async () => { await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.2 }).promise; + sessionReplay.sessionTargetingMatch = true; jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => true); - const shouldRecord = sessionReplay.getShouldRecord(); + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(true); }); + test('should set record as false if sessionTargetingMatch is false', async () => { + await sessionReplay.init(apiKey, { ...mockOptions, optOut: true }).promise; + sessionReplay.sessionTargetingMatch = false; + const shouldRecord = sessionReplay.getShouldCapture(); + expect(shouldRecord).toBe(false); + }); test('should set record as false if opt out in config', async () => { await sessionReplay.init(apiKey, { ...mockOptions, optOut: true }).promise; - const shouldRecord = sessionReplay.getShouldRecord(); + sessionReplay.sessionTargetingMatch = true; + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('should set record as false if no session id', async () => { await sessionReplay.init(apiKey, { ...mockOptions, sessionId: undefined }).promise; - const shouldRecord = sessionReplay.getShouldRecord(); + sessionReplay.sessionTargetingMatch = true; + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('opt out in config should override the sample rate', async () => { jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.8, optOut: true }).promise; - const shouldRecord = sessionReplay.getShouldRecord(); + sessionReplay.sessionTargetingMatch = true; + const shouldRecord = sessionReplay.getShouldCapture(); expect(shouldRecord).toBe(false); }); test('should return false if no config', async () => { - const shouldRecord = sessionReplay.getShouldRecord(); + const shouldRecord = sessionReplay.getShouldCapture(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.warn).not.toHaveBeenCalled(); expect(shouldRecord).toBe(false); @@ -790,7 +747,7 @@ describe('SessionReplay', () => { }); }); - describe('recordEvents', () => { + describe('captureEvents', () => { test('should return early if no config', async () => { await sessionReplay.init(apiKey, mockOptions).promise; const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] @@ -798,7 +755,7 @@ describe('SessionReplay', () => { record.mockReset(); sessionReplay.config = undefined; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); expect(record).not.toHaveBeenCalled(); if (!sessionReplay.eventsManager) { throw new Error('Did not call init'); @@ -810,7 +767,7 @@ describe('SessionReplay', () => { await sessionReplay.init(apiKey, mockOptions).promise; record.mockReset(); sessionReplay.identifiers = undefined; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); expect(record).not.toHaveBeenCalled(); }); @@ -819,7 +776,7 @@ describe('SessionReplay', () => { .promise; const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] .value; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); expect(record).not.toHaveBeenCalled(); if (!sessionReplay.eventsManager) { throw new Error('Did not call init'); @@ -832,7 +789,7 @@ describe('SessionReplay', () => { await sessionReplay.init(apiKey, mockOptions).promise; const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] .value; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); if (!sessionReplay.eventsManager) { throw new Error('Did not call init'); } @@ -850,19 +807,11 @@ describe('SessionReplay', () => { }); }); - test('should stop recording before starting anew', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - const stopRecordingMock = jest.fn(); - sessionReplay.recordCancelCallback = stopRecordingMock; - sessionReplay.recordEvents(); - expect(stopRecordingMock).toHaveBeenCalled(); - }); - test('should stop recording and send events if user opts out during recording', async () => { await sessionReplay.init(apiKey, mockOptions).promise; const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] .value; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); const stopRecordingMock = jest.fn(); sessionReplay.recordCancelCallback = stopRecordingMock; if (!sessionReplay.eventsManager) { @@ -888,7 +837,7 @@ describe('SessionReplay', () => { test('should add an error handler', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); const recordArg = record.mock.calls[0][0]; const errorHandlerReturn = recordArg?.errorHandler && recordArg?.errorHandler(new Error('test error')); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -899,7 +848,7 @@ describe('SessionReplay', () => { test('should rethrow CSSStylesheet errors', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); const recordArg = record.mock.calls[0][0]; const stylesheetErrorMessage = "Failed to execute 'insertRule' on 'CSSStyleSheet': Failed to parse the rule 'body::-ms-expand{display: none}"; @@ -911,7 +860,7 @@ describe('SessionReplay', () => { test('should rethrow external errors', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.recordEvents(); + sessionReplay.captureEvents(); const recordArg = record.mock.calls[0][0]; const error = new Error('test') as Error & { _external_?: boolean }; error._external_ = true; @@ -921,6 +870,106 @@ describe('SessionReplay', () => { }); }); + describe('evaluateTargetingAndCapture', () => { + let sessionReplay: SessionReplay; + let db: IDBPDatabase; + beforeEach(async () => { + const mockConfigWithTargeting: SessionReplayJoinedConfig = { + ...mockConfig, + optOut: mockConfig.optOut, + targetingConfig: flagConfig, + }; + const generateJoinedConfigFn = jest.fn().mockResolvedValue(mockConfigWithTargeting); + jest.spyOn(JoinedConfigGenerator, 'createSessionReplayJoinedConfigGenerator').mockResolvedValue({ + generateJoinedConfig: generateJoinedConfigFn, + } as unknown as SessionReplayJoinedConfigGenerator); + jest.spyOn(TargetingManager, 'evaluateTargetingAndStore').mockResolvedValue(false); + db = await targetingIDBStore.openOrCreateDB('static_key'); + await db.clear('sessionTargetingMatch'); + sessionReplay = new SessionReplay(); + // Don't call init as we normally require, as that calls this method and + // creates confusing test cases + sessionReplay.config = mockConfigWithTargeting; + sessionReplay.identifiers = { + sessionId: 123, + deviceId: '1a2b3c', + }; + sessionReplay.loggerProvider = mockLoggerProvider; + }); + test('should return undefined if no identifiers set', async () => { + const evaluateTargetingMock = jest.spyOn(TargetingManager, 'evaluateTargetingAndStore'); + sessionReplay.identifiers = undefined; + await sessionReplay.evaluateTargetingAndCapture(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.warn).toHaveBeenCalledWith( + 'Session replay init has not been called, cannot evaluate targeting.', + ); + expect(evaluateTargetingMock).not.toHaveBeenCalled(); + }); + + test('should not record if evaluateTargetingAndStore returns false', async () => { + jest.spyOn(TargetingManager, 'evaluateTargetingAndStore').mockResolvedValueOnce(false); + await sessionReplay.evaluateTargetingAndCapture(); + return evaluateTargetingAndStorePromise.then(() => { + expect(record).not.toHaveBeenCalled(); + }); + }); + + test('should record if evaluateTargetingAndStore returns true', async () => { + jest.spyOn(TargetingManager, 'evaluateTargetingAndStore').mockResolvedValueOnce(true); + await sessionReplay.evaluateTargetingAndCapture(); + return evaluateTargetingAndStorePromise.then(() => { + console.log('in check'); + expect(record).toHaveBeenCalled(); + }); + }); + + test('should pass event to evaluateTargetingAndStore', async () => { + await sessionReplay.evaluateTargetingAndCapture({ event: { event_type: 'Purchase' } }); + return evaluateTargetingAndStorePromise.then(() => { + expect(TargetingManager.evaluateTargetingAndStore).toHaveBeenCalledWith( + expect.objectContaining({ + targetingParams: { event: { event_type: 'Purchase' }, userProperties: undefined }, + }), + ); + }); + }); + + test('should pass user properties to evaluateTargetingAndStore', async () => { + await sessionReplay.evaluateTargetingAndCapture({ + event: { event_type: 'Purchase' }, + userProperties: { plan_id: 'free' }, + }); + return evaluateTargetingAndStorePromise.then(() => { + expect(TargetingManager.evaluateTargetingAndStore).toHaveBeenCalledWith( + expect.objectContaining({ + targetingParams: { event: { event_type: 'Purchase' }, userProperties: { plan_id: 'free' } }, + }), + ); + }); + }); + + test('should not pass event to evaluateTargetingAndStore if it is one of the SpecialEvents', async () => { + await sessionReplay.evaluateTargetingAndCapture({ event: { event_type: '$identify' } }); + expect(TargetingManager.evaluateTargetingAndStore).toHaveBeenCalledWith( + expect.objectContaining({ + targetingParams: { event: undefined, userProperties: undefined }, + }), + ); + }); + + test('should not call rrweb record more than once', async () => { + jest.spyOn(TargetingManager, 'evaluateTargetingAndStore').mockResolvedValue(true); + await sessionReplay.evaluateTargetingAndCapture(); + await sessionReplay.evaluateTargetingAndCapture(); + + expect(record).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.debug).toHaveBeenCalledWith( + 'captureEvents method fired - Session Replay capture already in progress.', + ); + }); + }); + describe('getDeviceId', () => { test('should return undefined if no config set', () => { expect(sessionReplay.getDeviceId()).toEqual(undefined); diff --git a/packages/session-replay-browser/test/targeting/targeting-idb-store.test.ts b/packages/session-replay-browser/test/targeting/targeting-idb-store.test.ts new file mode 100644 index 000000000..1c8084ae4 --- /dev/null +++ b/packages/session-replay-browser/test/targeting/targeting-idb-store.test.ts @@ -0,0 +1,186 @@ +import { Logger } from '@amplitude/analytics-types'; +import { IDBPDatabase } from 'idb'; +import { SessionReplayTargetingDB, targetingIDBStore } from '../../src/targeting/targeting-idb-store'; + +type MockedLogger = jest.Mocked; + +const apiKey = 'static_key'; + +describe('TargetingIDBStore', () => { + const mockLoggerProvider: MockedLogger = { + error: jest.fn(), + log: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + let db: IDBPDatabase; + beforeEach(async () => { + db = await targetingIDBStore.openOrCreateDB('static_key'); + await db.clear('sessionTargetingMatch'); + jest.useFakeTimers(); + }); + afterEach(() => { + jest.resetAllMocks(); + jest.useRealTimers(); + }); + + describe('getTargetingMatchForSession', () => { + test('should return the targeting match from idb store', async () => { + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + targetingMatch: true, + }); + const targetingMatch = await targetingIDBStore.getTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + }); + expect(targetingMatch).toEqual(true); + }); + test('should return undefined if no matching entry in the store', async () => { + const targetingMatch = await targetingIDBStore.getTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + }); + expect(targetingMatch).toEqual(undefined); + }); + test('should catch errors', async () => { + const mockDB: IDBPDatabase = { + get: jest.fn().mockImplementation(() => Promise.reject('error')), + } as unknown as IDBPDatabase; + jest.spyOn(targetingIDBStore, 'openOrCreateDB').mockResolvedValueOnce(mockDB); + await targetingIDBStore.getTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( + 'Failed to get targeting match for session id 123: error', + ); + }); + }); + + describe('storeTargetingMatchForSession', () => { + test('should add the targeting match to idb store', async () => { + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + targetingMatch: true, + }); + const targetingMatch = await targetingIDBStore.getTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + }); + expect(targetingMatch).toEqual(true); + }); + test('should catch errors', async () => { + const mockDB: IDBPDatabase = { + put: jest.fn().mockImplementation(() => Promise.reject('error')), + } as unknown as IDBPDatabase; + jest.spyOn(targetingIDBStore, 'openOrCreateDB').mockResolvedValueOnce(mockDB); + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + sessionId: 123, + apiKey, + targetingMatch: true, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( + 'Failed to store targeting match for session id 123: error', + ); + }); + }); + + describe('clearStoreOfOldSessions', () => { + test('should delete object stores with sessions older than 2 days', async () => { + // Set current time to 08:30 + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); + // Current session from one hour before, 07:30 + const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + apiKey, + sessionId: currentSessionId, + targetingMatch: true, + }); + // Add session from the same day + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + apiKey, + sessionId: new Date('2023-07-31 05:30:00').getTime(), + targetingMatch: true, + }); + // Add session from one month ago + await targetingIDBStore.storeTargetingMatchForSession({ + loggerProvider: mockLoggerProvider, + apiKey, + sessionId: new Date('2023-06-31 10:30:00').getTime(), + targetingMatch: true, + }); + const allEntries = + targetingIDBStore.dbs && (await targetingIDBStore.dbs['static_key'].getAll('sessionTargetingMatch')); + expect(allEntries).toEqual([ + { + sessionId: new Date('2023-06-31 10:30:00').getTime(), + targetingMatch: true, + }, + { + sessionId: new Date('2023-07-31 05:30:00').getTime(), + targetingMatch: true, + }, + { + sessionId: currentSessionId, + targetingMatch: true, + }, + ]); + + await targetingIDBStore.clearStoreOfOldSessions({ + loggerProvider: mockLoggerProvider, + apiKey, + currentSessionId, + }); + + const allEntriesUpdated = + targetingIDBStore.dbs && (await targetingIDBStore.dbs['static_key'].getAll('sessionTargetingMatch')); + // Only one month old entry should be deleted + expect(allEntriesUpdated).toEqual([ + { + sessionId: new Date('2023-07-31 05:30:00').getTime(), + targetingMatch: true, + }, + { + sessionId: currentSessionId, + targetingMatch: true, + }, + ]); + }); + test('should catch errors', async () => { + const mockDB: IDBPDatabase = { + transaction: jest.fn().mockImplementation(() => { + throw new Error('error'); + }), + } as unknown as IDBPDatabase; + jest.spyOn(targetingIDBStore, 'openOrCreateDB').mockResolvedValueOnce(mockDB); + await targetingIDBStore.clearStoreOfOldSessions({ + loggerProvider: mockLoggerProvider, + currentSessionId: 123, + apiKey, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( + 'Failed to clear old targeting matches for sessions: Error: error', + ); + }); + }); +}); diff --git a/packages/session-replay-browser/test/targeting/targeting-manager.test.ts b/packages/session-replay-browser/test/targeting/targeting-manager.test.ts new file mode 100644 index 000000000..40dbafb7a --- /dev/null +++ b/packages/session-replay-browser/test/targeting/targeting-manager.test.ts @@ -0,0 +1,135 @@ +import { Logger } from '@amplitude/analytics-types'; +import * as Targeting from '@amplitude/targeting'; +import { IDBPDatabase } from 'idb'; +import { SessionReplayJoinedConfig } from '../../src/config/types'; +import { SessionReplayTargetingDB, targetingIDBStore } from '../../src/targeting/targeting-idb-store'; +import { evaluateTargetingAndStore } from '../../src/targeting/targeting-manager'; +import { flagConfig } from '../flag-config-data'; + +type MockedLogger = jest.Mocked; + +jest.mock('@amplitude/targeting'); +type MockedTargeting = jest.Mocked; + +describe('Targeting Manager', () => { + const { evaluateTargeting } = Targeting as MockedTargeting; + let originalFetch: typeof global.fetch; + const mockLoggerProvider: MockedLogger = { + error: jest.fn(), + log: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + const config: SessionReplayJoinedConfig = { + apiKey: 'static_key', + loggerProvider: mockLoggerProvider, + sampleRate: 1, + targetingConfig: flagConfig, + } as unknown as SessionReplayJoinedConfig; + let db: IDBPDatabase; + beforeEach(async () => { + db = await targetingIDBStore.openOrCreateDB('static_key'); + await db.clear('sessionTargetingMatch'); + jest.useFakeTimers(); + originalFetch = global.fetch; + global.fetch = jest.fn(() => + Promise.resolve({ + status: 200, + }), + ) as jest.Mock; + }); + afterEach(() => { + jest.resetAllMocks(); + global.fetch = originalFetch; + + jest.useRealTimers(); + }); + + describe('evaluateTargetingAndStore', () => { + let storeTargetingMatchForSessionMock: jest.SpyInstance; + let getTargetingMatchForSessionMock: jest.SpyInstance; + beforeEach(() => { + storeTargetingMatchForSessionMock = jest.spyOn(targetingIDBStore, 'storeTargetingMatchForSession'); + getTargetingMatchForSessionMock = jest.spyOn(targetingIDBStore, 'getTargetingMatchForSession'); + }); + test('should return a true match from IndexedDB', async () => { + jest.spyOn(targetingIDBStore, 'getTargetingMatchForSession').mockResolvedValueOnce(true); + const sessionTargetingMatch = await evaluateTargetingAndStore({ + sessionId: 123, + loggerProvider: config.loggerProvider, + apiKey: config.apiKey, + targetingConfig: flagConfig, + }); + expect(getTargetingMatchForSessionMock).toHaveBeenCalled(); + expect(evaluateTargeting).not.toHaveBeenCalled(); + expect(sessionTargetingMatch).toBe(true); + }); + + test('should use remote config to determine targeting match', async () => { + jest.spyOn(targetingIDBStore, 'getTargetingMatchForSession').mockResolvedValueOnce(false); + evaluateTargeting.mockResolvedValueOnce({ + sr_targeting_config: { + key: 'on', + }, + }); + const mockUserProperties = { + country: 'US', + city: 'San Francisco', + }; + const sessionTargetingMatch = await evaluateTargetingAndStore({ + sessionId: 123, + loggerProvider: config.loggerProvider, + apiKey: config.apiKey, + targetingConfig: flagConfig, + targetingParams: { + userProperties: mockUserProperties, + }, + }); + expect(evaluateTargeting).toHaveBeenCalledWith({ + apiKey: 'static_key', + loggerProvider: mockLoggerProvider, + flag: flagConfig, + sessionId: 123, + userProperties: mockUserProperties, + }); + expect(sessionTargetingMatch).toBe(true); + }); + test('should store sessionTargetingMatch', async () => { + evaluateTargeting.mockResolvedValueOnce({ + sr_targeting_config: { + key: 'on', + }, + }); + const sessionTargetingMatch = await evaluateTargetingAndStore({ + sessionId: 123, + loggerProvider: config.loggerProvider, + apiKey: config.apiKey, + targetingConfig: flagConfig, + }); + expect(storeTargetingMatchForSessionMock).toHaveBeenCalledWith({ + targetingMatch: true, + sessionId: 123, + apiKey: config.apiKey, + loggerProvider: mockLoggerProvider, + }); + expect(sessionTargetingMatch).toBe(true); + }); + test('should handle error', async () => { + jest.spyOn(targetingIDBStore, 'storeTargetingMatchForSession').mockImplementationOnce(() => { + throw new Error('storage error'); + }); + const sessionTargetingMatch = await evaluateTargetingAndStore({ + sessionId: 123, + loggerProvider: config.loggerProvider, + apiKey: config.apiKey, + targetingConfig: flagConfig, + }); + expect(sessionTargetingMatch).toBe(true); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual('storage error'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3b7581d33..3804f0771 100644 --- a/yarn.lock +++ b/yarn.lock @@ -160,14 +160,6 @@ resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz#26441a0fb2e956a64e4ede50fb80b848179bb5db" integrity sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA== -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1141,14 +1133,7 @@ dependencies: tslib "^2.3.1" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/code-frame@^7.24.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -1156,38 +1141,12 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" - integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== - -"@babel/compat-data@^7.24.7": +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.18.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" - integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.5" - "@babel/parser" "^7.20.5" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/core@^7.24.7": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.18.5", "@babel/core@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -1208,16 +1167,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.14.0", "@babel/generator@^7.20.5", "@babel/generator@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" - integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== - dependencies: - "@babel/types" "^7.20.5" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.24.7": +"@babel/generator@^7.14.0", "@babel/generator@^7.24.7", "@babel/generator@^7.7.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -1242,17 +1192,7 @@ "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" - integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== - dependencies: - "@babel/compat-data" "^7.20.0" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" - semver "^6.3.0" - -"@babel/helper-compilation-targets@^7.24.7": +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9" integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg== @@ -1296,12 +1236,7 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-environment-visitor@^7.24.7": +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== @@ -1315,15 +1250,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - -"@babel/helper-function-name@^7.24.7": +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== @@ -1331,14 +1258,7 @@ "@babel/template" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-hoist-variables@^7.24.7": +"@babel/helper-hoist-variables@^7.18.6", "@babel/helper-hoist-variables@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== @@ -1352,14 +1272,7 @@ dependencies: "@babel/types" "^7.18.9" -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-imports@^7.24.7": +"@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== @@ -1367,21 +1280,7 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" - integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" - -"@babel/helper-module-transforms@^7.24.7": +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ== @@ -1399,12 +1298,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-plugin-utils@^7.24.7": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== @@ -1430,13 +1324,6 @@ "@babel/traverse" "^7.19.1" "@babel/types" "^7.19.0" -"@babel/helper-simple-access@^7.19.4", "@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - "@babel/helper-simple-access@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" @@ -1452,46 +1339,24 @@ dependencies: "@babel/types" "^7.20.0" -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-split-export-declaration@^7.24.7": +"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== dependencies: "@babel/types" "^7.24.7" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - "@babel/helper-string-parser@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.24.7": +"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - -"@babel/helper-validator-option@^7.24.7": +"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== @@ -1506,15 +1371,6 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.20.5": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" - integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - "@babel/helpers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" @@ -1523,15 +1379,6 @@ "@babel/template" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -1542,12 +1389,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== - -"@babel/parser@^7.24.7": +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== @@ -1980,16 +1822,7 @@ "@babel/helper-module-transforms" "^7.19.6" "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" - integrity sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ== - dependencies: - "@babel/helper-module-transforms" "^7.19.6" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-simple-access" "^7.19.4" - -"@babel/plugin-transform-modules-commonjs@^7.24.7": +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6", "@babel/plugin-transform-modules-commonjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab" integrity sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ== @@ -2327,16 +2160,7 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.3.3": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" - -"@babel/template@^7.24.7": +"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.24.7", "@babel/template@^7.3.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== @@ -2345,23 +2169,7 @@ "@babel/parser" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/traverse@^7.14.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.7.2": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" - integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.5" - "@babel/types" "^7.20.5" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.24.7": +"@babel/traverse@^7.14.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.24.7", "@babel/traverse@^7.7.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -2377,16 +2185,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" - integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@babel/types@^7.24.7": +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.24.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== @@ -2828,14 +2627,7 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" -"@jest/schemas@^29.0.0": - version "29.0.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" - integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== - dependencies: - "@sinclair/typebox" "^0.24.1" - -"@jest/schemas@^29.6.3": +"@jest/schemas@^29.0.0", "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== @@ -2871,28 +2663,7 @@ jest-haste-map "^29.3.1" slash "^3.0.0" -"@jest/transform@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.3.1.tgz#1e6bd3da4af50b5c82a539b7b1f3770568d6e36d" - integrity sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.3.1" - "@jridgewell/trace-mapping" "^0.3.15" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.1" - -"@jest/transform@^29.7.0": +"@jest/transform@^29.3.1", "@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== @@ -2935,19 +2706,7 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jest/types@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3" - integrity sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA== - dependencies: - "@jest/schemas" "^29.0.0" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jest/types@^29.6.3": +"@jest/types@^29.3.1", "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -2959,24 +2718,7 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== @@ -2985,21 +2727,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - "@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" @@ -3013,12 +2745,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -3031,15 +2758,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -4308,11 +4027,6 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== -"@sinclair/typebox@^0.24.1": - version "0.24.51" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" - integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -5214,20 +4928,7 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" - integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== - dependencies: - "@jest/transform" "^29.3.1" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.2.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-jest@^29.7.0: +babel-jest@^29.3.1, babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -5265,16 +4966,6 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz#23ee99c37390a98cfddf3ef4a78674180d823094" - integrity sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" @@ -5639,14 +5330,6 @@ babel-preset-fbjs@^3.4.0: "@babel/plugin-transform-template-literals" "^7.0.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" -babel-preset-jest@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz#3048bea3a1af222e3505e4a767a974c95a7620dc" - integrity sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA== - dependencies: - babel-plugin-jest-hoist "^29.2.0" - babel-preset-current-node-syntax "^1.0.0" - babel-preset-jest@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" @@ -5839,17 +5522,7 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" -browserslist@^4.20.4, browserslist@^4.21.3, browserslist@^4.21.4: - version "4.21.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" - integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== - dependencies: - caniuse-lite "^1.0.30001400" - electron-to-chromium "^1.4.251" - node-releases "^2.0.6" - update-browserslist-db "^1.0.9" - -browserslist@^4.22.2: +browserslist@^4.20.4, browserslist@^4.21.4, browserslist@^4.22.2: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== @@ -6003,12 +5676,7 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001400: - version "1.0.30001439" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" - integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== - -caniuse-lite@^1.0.30001629: +caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001629: version "1.0.30001633" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz#45a4ade9fb9ec80a06537a6271ac1e0afadcb324" integrity sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg== @@ -6032,7 +5700,7 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -6469,7 +6137,7 @@ conventional-recommended-bump@^6.1.0: meow "^8.0.0" q "^1.5.1" -convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.5.1, convert-source-map@^1.6.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== @@ -6632,10 +6300,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" @@ -6646,13 +6314,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -6893,12 +6554,7 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" -electron-to-chromium@^1.3.47, electron-to-chromium@^1.4.251: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== - -electron-to-chromium@^1.4.796: +electron-to-chromium@^1.3.47, electron-to-chromium@^1.4.796: version "1.4.802" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz#49b397eadc95a49b1ac33eebee146b8e5a93773f" integrity sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA== @@ -6986,12 +6642,7 @@ errorhandler@^1.5.0: accepts "~1.3.7" escape-html "~1.0.3" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escalade@^3.1.2: +escalade@^3.1.1, escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== @@ -8780,26 +8431,7 @@ jest-get-type@^29.2.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== -jest-haste-map@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.3.1.tgz#af83b4347f1dae5ee8c2fb57368dc0bb3e5af843" - integrity sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A== - dependencies: - "@jest/types" "^29.3.1" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" - jest-worker "^29.3.1" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-haste-map@^29.7.0: +jest-haste-map@^29.3.1, jest-haste-map@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== @@ -8870,12 +8502,7 @@ jest-regex-util@^27.0.6: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-regex-util@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" - integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== - -jest-regex-util@^29.6.3: +jest-regex-util@^29.2.0, jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== @@ -9008,19 +8635,7 @@ jest-util@^27.2.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.0.0, jest-util@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" - integrity sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ== - dependencies: - "@jest/types" "^29.3.1" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-util@^29.7.0: +jest-util@^29.0.0, jest-util@^29.3.1, jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== @@ -9088,17 +8703,7 @@ jest-worker@^27.2.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.3.1.tgz#e9462161017a9bb176380d721cab022661da3d6b" - integrity sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw== - dependencies: - "@types/node" "*" - jest-util "^29.3.1" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.7.0: +jest-worker@^29.3.1, jest-worker@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== @@ -9293,12 +8898,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== - -json5@^2.2.3: +json5@^2.2.1, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -10416,14 +10016,7 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.12: +node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -10470,11 +10063,6 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -node-releases@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.7.tgz#593edbc7c22860ee4d32d3933cfebdfab0c0e0e5" - integrity sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ== - node-stream-zip@^1.9.1: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" @@ -11099,12 +10687,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picocolors@^1.0.1: +picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== @@ -11877,14 +11460,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.5.5, rxjs@^7.5.7: - version "7.6.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.6.0.tgz#361da5362b6ddaa691a2de0b4f2d32028f1eb5a2" - integrity sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ== - dependencies: - tslib "^2.1.0" - -rxjs@^7.8.1: +rxjs@^7.5.5, rxjs@^7.5.7, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -11958,12 +11534,7 @@ semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^ dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^6.3.1: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -13024,14 +12595,6 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" -update-browserslist-db@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" From 1bdcde094b45151a720c2d76ba47ad2129d8d043 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Fri, 23 Aug 2024 14:59:50 -0400 Subject: [PATCH 2/3] fix(session replay): update version --- packages/plugin-session-replay-browser/package.json | 6 +----- packages/plugin-session-replay-browser/src/version.ts | 2 +- packages/session-replay-browser/package.json | 2 +- packages/session-replay-browser/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index e495b6f23..ee3f699db 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,10 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", -<<<<<<< HEAD - "version": "1.6.22", -======= - "version": "1.7.0-srtargeting.0", ->>>>>>> 272189c9 (feat(session replay): add ability to capture replays based on targeting via remote config) + "version": "1.7.0-srtargeting.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts index c2c907c27..9b533ff40 100644 --- a/packages/plugin-session-replay-browser/src/version.ts +++ b/packages/plugin-session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.7.0-srtargeting.0'; +export const VERSION = '1.7.0-srtargeting.1'; diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 71d26a1f2..209eae44a 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "1.14.0-srtargeting.0", + "version": "1.14.0-srtargeting.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/session-replay-browser/src/version.ts b/packages/session-replay-browser/src/version.ts index 307a8abeb..141d4c654 100644 --- a/packages/session-replay-browser/src/version.ts +++ b/packages/session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.14.0-srtargeting.0'; +export const VERSION = '1.14.0-srtargeting.1'; From 6fa590f6c4e09e8cc0748aa0b04ac74345e2175c Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 23 Aug 2024 19:08:23 +0000 Subject: [PATCH 3/3] chore(release): publish - @amplitude/plugin-session-replay-browser@1.7.0-srtargeting.2 - @amplitude/session-replay-browser@1.14.0-srtargeting.2 --- .../plugin-session-replay-browser/CHANGELOG.md | 17 +++++++++++++++++ .../plugin-session-replay-browser/package.json | 4 ++-- .../src/version.ts | 2 +- packages/session-replay-browser/CHANGELOG.md | 17 +++++++++++++++++ packages/session-replay-browser/package.json | 2 +- packages/session-replay-browser/src/version.ts | 2 +- 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 152f17431..5d5ffbf3a 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.7.0-srtargeting.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.6.22...@amplitude/plugin-session-replay-browser@1.7.0-srtargeting.2) (2024-08-23) + +### Bug Fixes + +- **session replay:** update version + ([1bdcde0](https://github.com/amplitude/Amplitude-TypeScript/commit/1bdcde094b45151a720c2d76ba47ad2129d8d043)) + +### Features + +- **session replay:** add ability to capture replays based on targeting via remote config + ([3daf644](https://github.com/amplitude/Amplitude-TypeScript/commit/3daf644e3fa471d88c5083375d745ace52b9f4f0)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.6.22](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.6.21...@amplitude/plugin-session-replay-browser@1.6.22) (2024-08-23) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index ee3f699db..8c882d55e 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "1.7.0-srtargeting.1", + "version": "1.7.0-srtargeting.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^1.14.0-srtargeting.0", + "@amplitude/session-replay-browser": "^1.14.0-srtargeting.2", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts index 9b533ff40..55fccfaff 100644 --- a/packages/plugin-session-replay-browser/src/version.ts +++ b/packages/plugin-session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.7.0-srtargeting.1'; +export const VERSION = '1.7.0-srtargeting.2'; diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 9bf13906f..9cad312e9 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.14.0-srtargeting.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@1.13.6...@amplitude/session-replay-browser@1.14.0-srtargeting.2) (2024-08-23) + +### Bug Fixes + +- **session replay:** update version + ([1bdcde0](https://github.com/amplitude/Amplitude-TypeScript/commit/1bdcde094b45151a720c2d76ba47ad2129d8d043)) + +### Features + +- **session replay:** add ability to capture replays based on targeting via remote config + ([3daf644](https://github.com/amplitude/Amplitude-TypeScript/commit/3daf644e3fa471d88c5083375d745ace52b9f4f0)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.13.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@1.13.5...@amplitude/session-replay-browser@1.13.6) (2024-08-23) **Note:** Version bump only for package @amplitude/session-replay-browser diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 209eae44a..d819f8cb3 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "1.14.0-srtargeting.1", + "version": "1.14.0-srtargeting.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/session-replay-browser/src/version.ts b/packages/session-replay-browser/src/version.ts index 141d4c654..6f809f520 100644 --- a/packages/session-replay-browser/src/version.ts +++ b/packages/session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.14.0-srtargeting.1'; +export const VERSION = '1.14.0-srtargeting.2';