From c181d3657aa167e3a9df9783cfbbfd4ff117ca7e Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 18 Nov 2021 12:48:11 -0800 Subject: [PATCH 01/68] initial impl; need update logic, tests, deployment --- packages/core/CHANGELOG.md | 0 packages/core/jest.config.js | 21 +++++++ packages/core/package.json | 32 ++++++++++ packages/core/rollup.config.js | 43 ++++++++++++++ packages/core/src/amplitudeCore.ts | 18 ++++++ packages/core/src/analyticsConnector.ts | 41 +++++++++++++ packages/core/src/identityStore.ts | 78 +++++++++++++++++++++++++ packages/core/src/index.ts | 12 ++++ packages/core/test/client.test.ts | 0 packages/core/tsconfig.json | 24 ++++++++ packages/core/tsconfig.test.json | 13 +++++ 11 files changed, 282 insertions(+) create mode 100644 packages/core/CHANGELOG.md create mode 100644 packages/core/jest.config.js create mode 100644 packages/core/package.json create mode 100644 packages/core/rollup.config.js create mode 100644 packages/core/src/amplitudeCore.ts create mode 100644 packages/core/src/analyticsConnector.ts create mode 100644 packages/core/src/identityStore.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/test/client.test.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsconfig.test.json diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js new file mode 100644 index 00000000..d1e5f0e6 --- /dev/null +++ b/packages/core/jest.config.js @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { pathsToModuleNameMapper } = require('ts-jest/utils'); + +const package = require('./package'); +const { compilerOptions } = require('./tsconfig.test.json'); + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + displayName: package.name, + name: package.name, + rootDir: '.', + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/', + }), + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.test.json', + }, + }, +}; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..7206e5b1 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,32 @@ +{ + "name": "@amplitude/amplitude-core", + "version": "0.0.1", + "description": "Core package for Amplitide SDKs", + "main": "dist/core.umd.js", + "types": "dist/types/src/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "rm -rf dist && rollup -c", + "docs": "typedoc", + "lint": "eslint . --ignore-path ../../.eslintignore && prettier -c . --ignore-path ../../.prettierignore", + "test": "jest", + "version": "yarn docs && git add ../../docs", + "prepublish": "yarn build" + }, + "repository": { + "type": "git", + "url": "https://github.com/amplitude/experiment-js-client.git", + "directory": "packages/core" + }, + "author": "Amplitude", + "license": "MIT", + "private": false, + "bugs": { + "url": "https://github.com/amplitude/experiment-js-client/issues" + }, + "homepage": "https://github.com/amplitude/experiment-js-client#readme", + "dependencies": {}, + "gitHead": "0a910f04a64dafcf37b68be45ed7dca58fdd6acf" +} diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js new file mode 100644 index 00000000..03960f3e --- /dev/null +++ b/packages/core/rollup.config.js @@ -0,0 +1,43 @@ +import babel from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import typescript from '@rollup/plugin-typescript'; + +import tsConfig from './tsconfig.json'; + +const browserConfig = { + input: 'src/index.ts', + output: { + dir: 'dist', + entryFileNames: 'experiment.umd.js', + exports: 'named', + format: 'umd', + name: 'Experiment', + }, + treeshake: { + moduleSideEffects: 'no-external', + }, + external: [], + plugins: [ + replace({ BUILD_BROWSER: true }), + resolve(), + json(), + commonjs(), + typescript({ + declaration: true, + declarationDir: 'dist/types', + include: tsConfig.include, + rootDir: '.', + }), + babel({ + babelHelpers: 'bundled', + exclude: ['node_modules/**'], + }), + ], +}; + +const configs = [browserConfig]; + +export default configs; diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts new file mode 100644 index 00000000..6f30815a --- /dev/null +++ b/packages/core/src/amplitudeCore.ts @@ -0,0 +1,18 @@ +import { + AnalyticsConnector, + AnalyticsConnectorImpl, +} from './analyticsConnector'; +import { IdentityStore, IdentityStoreImpl } from './identityStore'; + +export class AmplitudeCore { + identityStore: IdentityStore = new IdentityStoreImpl(); + analyticsConnector: AnalyticsConnector = new AnalyticsConnectorImpl(); + + private static instances: Record = {}; + static getInstance(instanceName: string): AmplitudeCore { + if (!AmplitudeCore.instances[instanceName]) { + AmplitudeCore.instances[instanceName] = new AmplitudeCore(); + } + return AmplitudeCore.instances[instanceName]; + } +} diff --git a/packages/core/src/analyticsConnector.ts b/packages/core/src/analyticsConnector.ts new file mode 100644 index 00000000..68782965 --- /dev/null +++ b/packages/core/src/analyticsConnector.ts @@ -0,0 +1,41 @@ +export type AnalyticsEvent = { + eventType: string; + eventProperties?: Record; + userProperties?: Record; +}; + +export type AnalyticsEventListener = (event: AnalyticsEvent) => void; + +export interface AnalyticsConnector { + logEvent(event: AnalyticsEvent): void; + addEventListener(listener: AnalyticsEventListener): void; + removeEventListener(listener: AnalyticsEventListener): void; +} + +export class AnalyticsConnectorImpl implements AnalyticsConnector { + private listeners = new Set(); + private queue: AnalyticsEvent[] = []; + + logEvent(event: AnalyticsEvent): void { + if (this.listeners.size == 0) { + this.queue.push(event); + } else { + this.listeners.forEach((listener) => { + listener(event); + }); + } + } + + addEventListener(listener: AnalyticsEventListener): void { + this.listeners.add(listener); + if (this.queue.length > 0) { + this.queue.forEach((event) => { + listener(event); + }); + } + } + + removeEventListener(listener: AnalyticsEventListener): void { + this.listeners.delete(listener); + } +} diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts new file mode 100644 index 00000000..92fb3949 --- /dev/null +++ b/packages/core/src/identityStore.ts @@ -0,0 +1,78 @@ +export type Identity = { + userId?: string; + deviceId?: string; + userProperties?: Record; +}; + +export type IdentityListener = (identity: Identity) => void; + +export interface IdentityStore { + editIdentity(): IdentityEditor; + getIdentity(): Identity; + setIdentity(identity: Identity): void; + addIdentityListener(listener: IdentityListener): void; + removeIdentityListener(listener: IdentityListener): void; +} + +export interface IdentityEditor { + setUserId(userId: string): IdentityEditor; + setDeviceId(deviceId: string): IdentityEditor; + setUserProperties(userProperties: Record): IdentityEditor; + updateUserProperties( + actions: Record>, + ): IdentityEditor; + commit(): void; +} + +export class IdentityStoreImpl implements IdentityStore { + private identity: Identity = {}; + private listeners = new Set(); + + editIdentity(): IdentityEditor { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + const actingIdentity = Object.assign(this.identity); + const editor: IdentityEditor = { + setUserId: function (userId: string): IdentityEditor { + actingIdentity.userId = userId; + return this; + }, + setDeviceId: function (deviceId: string): IdentityEditor { + actingIdentity.deviceId = deviceId; + return this; + }, + setUserProperties: function ( + userProperties: Record, + ): IdentityEditor { + actingIdentity.userProperties = userProperties; + return this; + }, + updateUserProperties: function ( + actions: Record>, + ): IdentityEditor { + for (const [action, properties] of Object.entries(actions)) { + // TODO update logic + throw Error(`${action}, ${properties}`); + } + return this; + }, + commit: function (): void { + self.setIdentity(actingIdentity); + return this; + }, + }; + return editor; + } + getIdentity(): Identity { + return Object.assign(this.identity); + } + setIdentity(identity: Identity): void { + this.identity = Object.assign(identity); + } + addIdentityListener(listener: IdentityListener): void { + this.listeners.add(listener); + } + removeIdentityListener(listener: IdentityListener): void { + this.listeners.delete(listener); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..c1e60fd8 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,12 @@ +export { AmplitudeCore } from './amplitudeCore'; +export { + AnalyticsConnector, + AnalyticsEvent, + AnalyticsEventListener, +} from './analyticsConnector'; +export { + Identity, + IdentityStore, + IdentityListener, + Editor, +} from './identityStore'; diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..c694f848 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "package.json"], + "typedocOptions": { + "name": "Experiment JS Client Documentation", + "entryPoints": ["./src/index.ts"], + "categoryOrder": [ + "Core Usage", + "Configuration", + "Context Provider", + "Types" + ], + "categorizeByGroup": false, + "disableSources": true, + "excludePrivate": true, + "excludeProtected": true, + "excludeInternal": true, + "hideGenerator": true, + "includeVersion": true, + "out": "../../docs", + "readme": "none", + "theme": "minimal" + } +} diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json new file mode 100644 index 00000000..a6ff84d1 --- /dev/null +++ b/packages/core/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "rootDir": ".", + "baseUrl": ".", + "paths": { + "src/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["dist"] +} From 0de477c8e6813bf29900834443f74e12b4c6245d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 18 Nov 2021 13:41:56 -0800 Subject: [PATCH 02/68] fix export, add test files --- packages/core/src/index.ts | 2 +- .../core/test/{client.test.ts => analyticsConnector.test.ts} | 0 packages/core/test/identityStore.test.ts | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/test/{client.test.ts => analyticsConnector.test.ts} (100%) create mode 100644 packages/core/test/identityStore.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c1e60fd8..20d7a6c2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,5 +8,5 @@ export { Identity, IdentityStore, IdentityListener, - Editor, + IdentityEditor, } from './identityStore'; diff --git a/packages/core/test/client.test.ts b/packages/core/test/analyticsConnector.test.ts similarity index 100% rename from packages/core/test/client.test.ts rename to packages/core/test/analyticsConnector.test.ts diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts new file mode 100644 index 00000000..e69de29b From cc25e5aec15fe4c9db381ae7c53a48b2a8f83694 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 18 Nov 2021 13:52:45 -0800 Subject: [PATCH 03/68] add basic test case --- packages/core/src/identityStore.ts | 6 ++++++ packages/core/test/analyticsConnector.test.ts | 9 +++++++++ packages/core/test/identityStore.test.ts | 11 +++++++++++ 3 files changed, 26 insertions(+) diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index 92fb3949..5f2206ed 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -67,7 +67,13 @@ export class IdentityStoreImpl implements IdentityStore { return Object.assign(this.identity); } setIdentity(identity: Identity): void { + const originalIdentity = Object.assign(this.identity); this.identity = Object.assign(identity); + if (originalIdentity != identity) { + this.listeners.forEach((listener) => { + listener(identity); + }); + } } addIdentityListener(listener: IdentityListener): void { this.listeners.add(listener); diff --git a/packages/core/test/analyticsConnector.test.ts b/packages/core/test/analyticsConnector.test.ts index e69de29b..b6a9aaf1 100644 --- a/packages/core/test/analyticsConnector.test.ts +++ b/packages/core/test/analyticsConnector.test.ts @@ -0,0 +1,9 @@ +import { AnalyticsConnectorImpl } from "../src/analyticsConnector"; + +test('addEventListener, logEvent, listner called', async () => { + const analyticsConnector = new AnalyticsConnectorImpl(); + const expectedEvent = { eventType: 'test' }; + analyticsConnector.addEventListener((event) => { + expect(event).toEqual(expectedEvent); + }); +}); \ No newline at end of file diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index e69de29b..8fb37c5f 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -0,0 +1,11 @@ +import { IdentityStoreImpl } from "../src/identityStore"; + +test('editIdentity, setUserId setDeviceId, success', async () => { + const identityStore = new IdentityStoreImpl(); + identityStore.editIdentity() + .setUserId('user_id') + .setDeviceId('device_id') + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({userId: 'user_id', deviceId: 'device_id'}); +}); \ No newline at end of file From 1c8b8d92c95451b1629ea162e409e200c4d2232f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 19 Nov 2021 14:41:57 -0800 Subject: [PATCH 04/68] fix lint, add update user properties logic; TODO Tests --- .prettierignore | 1 - packages/core/src/analyticsConnector.ts | 24 +++--- packages/core/src/identityStore.ts | 73 ++++++++++++++++++- packages/core/src/index.ts | 2 +- packages/core/test/analyticsConnector.test.ts | 28 ++++++- packages/core/test/identityStore.test.ts | 17 ++++- 6 files changed, 117 insertions(+), 28 deletions(-) diff --git a/.prettierignore b/.prettierignore index 49e10669..94f48112 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ dist/ *.md -*.test.ts diff --git a/packages/core/src/analyticsConnector.ts b/packages/core/src/analyticsConnector.ts index 68782965..d52dd6af 100644 --- a/packages/core/src/analyticsConnector.ts +++ b/packages/core/src/analyticsConnector.ts @@ -4,38 +4,32 @@ export type AnalyticsEvent = { userProperties?: Record; }; -export type AnalyticsEventListener = (event: AnalyticsEvent) => void; +export type AnalyticsEventReceiver = (event: AnalyticsEvent) => void; export interface AnalyticsConnector { logEvent(event: AnalyticsEvent): void; - addEventListener(listener: AnalyticsEventListener): void; - removeEventListener(listener: AnalyticsEventListener): void; + setEventReceiver(listener: AnalyticsEventReceiver): void; } export class AnalyticsConnectorImpl implements AnalyticsConnector { - private listeners = new Set(); + private receiver: AnalyticsEventReceiver; private queue: AnalyticsEvent[] = []; logEvent(event: AnalyticsEvent): void { - if (this.listeners.size == 0) { + if (!this.receiver) { this.queue.push(event); } else { - this.listeners.forEach((listener) => { - listener(event); - }); + this.receiver(event); } } - addEventListener(listener: AnalyticsEventListener): void { - this.listeners.add(listener); + setEventReceiver(receiver: AnalyticsEventReceiver): void { + this.receiver = receiver; if (this.queue.length > 0) { this.queue.forEach((event) => { - listener(event); + receiver(event); }); + this.queue = []; } } - - removeEventListener(listener: AnalyticsEventListener): void { - this.listeners.delete(listener); - } } diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index 5f2206ed..2dcb8e75 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -1,3 +1,11 @@ +const ID_OP_SET = '$set'; +const ID_OP_UNSET = '$unset'; +const ID_OP_SET_ONCE = '$setOnce'; +const ID_OP_ADD = '$add'; +const ID_OP_APPEND = '$append'; +const ID_OP_PREPEND = '$prepend'; +const ID_OP_CLEAR_ALL = '$clearAll'; + export type Identity = { userId?: string; deviceId?: string; @@ -30,32 +38,85 @@ export class IdentityStoreImpl implements IdentityStore { editIdentity(): IdentityEditor { // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const actingIdentity = Object.assign(this.identity); + const self: IdentityStore = this; + const actingIdentity: Identity = Object.assign(this.identity); const editor: IdentityEditor = { setUserId: function (userId: string): IdentityEditor { actingIdentity.userId = userId; return this; }, + setDeviceId: function (deviceId: string): IdentityEditor { actingIdentity.deviceId = deviceId; return this; }, + setUserProperties: function ( userProperties: Record, ): IdentityEditor { actingIdentity.userProperties = userProperties; return this; }, + updateUserProperties: function ( actions: Record>, ): IdentityEditor { + let actingProperties = actingIdentity.userProperties || {}; for (const [action, properties] of Object.entries(actions)) { - // TODO update logic - throw Error(`${action}, ${properties}`); + switch (action) { + case ID_OP_SET: + for (const [key, value] of Object.entries(properties)) { + actingProperties[key] = value; + } + break; + case ID_OP_UNSET: + for (const key of Object.keys(properties)) { + delete actingProperties[key]; + } + break; + case ID_OP_SET_ONCE: + for (const [key, value] of Object.entries(properties)) { + if (!actingIdentity.userProperties[key]) { + actingProperties[key] = value; + } + } + break; + case ID_OP_ADD: + for (const [key, value] of Object.entries(properties)) { + const actingValue = actingProperties[key]; + if ( + typeof actingValue === 'number' && + typeof value === 'number' + ) { + actingProperties[key] = actingValue + value; + } + } + break; + case ID_OP_APPEND: + for (const [key, value] of Object.entries(properties)) { + const actingValue = actingProperties[key]; + if (Array.isArray(actingValue) && Array.isArray(value)) { + actingProperties[key] = actingValue.push(...value); + } + } + break; + case ID_OP_PREPEND: + for (const [key, value] of Object.entries(properties)) { + const actingValue = actingProperties[key]; + if (Array.isArray(actingValue) && Array.isArray(value)) { + actingProperties[key] = value.push(...actingValue); + } + } + break; + case ID_OP_CLEAR_ALL: + actingProperties = {}; + break; + } } + actingIdentity.userProperties = actingProperties; return this; }, + commit: function (): void { self.setIdentity(actingIdentity); return this; @@ -63,9 +124,11 @@ export class IdentityStoreImpl implements IdentityStore { }; return editor; } + getIdentity(): Identity { return Object.assign(this.identity); } + setIdentity(identity: Identity): void { const originalIdentity = Object.assign(this.identity); this.identity = Object.assign(identity); @@ -75,9 +138,11 @@ export class IdentityStoreImpl implements IdentityStore { }); } } + addIdentityListener(listener: IdentityListener): void { this.listeners.add(listener); } + removeIdentityListener(listener: IdentityListener): void { this.listeners.delete(listener); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20d7a6c2..e3f7b1ab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,7 @@ export { AmplitudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, - AnalyticsEventListener, + AnalyticsEventReceiver, } from './analyticsConnector'; export { Identity, diff --git a/packages/core/test/analyticsConnector.test.ts b/packages/core/test/analyticsConnector.test.ts index b6a9aaf1..430d1a00 100644 --- a/packages/core/test/analyticsConnector.test.ts +++ b/packages/core/test/analyticsConnector.test.ts @@ -1,9 +1,31 @@ -import { AnalyticsConnectorImpl } from "../src/analyticsConnector"; +import { AnalyticsConnectorImpl } from '../src/analyticsConnector'; test('addEventListener, logEvent, listner called', async () => { const analyticsConnector = new AnalyticsConnectorImpl(); const expectedEvent = { eventType: 'test' }; - analyticsConnector.addEventListener((event) => { + analyticsConnector.setEventReceiver((event) => { expect(event).toEqual(expectedEvent); }); -}); \ No newline at end of file +}); + +test('multiple logEvent, late addEventListener, listner called', async () => { + const expectedEvent0 = { eventType: 'test0' }; + const expectedEvent1 = { eventType: 'test1' }; + const expectedEvent2 = { eventType: 'test2' }; + const analyticsConnector = new AnalyticsConnectorImpl(); + analyticsConnector.logEvent(expectedEvent0); + analyticsConnector.logEvent(expectedEvent1); + analyticsConnector.logEvent(expectedEvent2); + let count = 0; + analyticsConnector.setEventReceiver((event) => { + if (count == 0) { + expect(event).toEqual(expectedEvent0); + } else if (count == 1) { + expect(event).toEqual(expectedEvent1); + } else if (count == 2) { + expect(event).toEqual(expectedEvent2); + } + count++; + }); + expect(count).toEqual(3); +}); diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index 8fb37c5f..389f582f 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -1,11 +1,20 @@ -import { IdentityStoreImpl } from "../src/identityStore"; +/* eslint-disable no-console */ +import { IdentityStoreImpl } from '../src/identityStore'; test('editIdentity, setUserId setDeviceId, success', async () => { const identityStore = new IdentityStoreImpl(); - identityStore.editIdentity() + identityStore + .editIdentity() .setUserId('user_id') .setDeviceId('device_id') .commit(); const identity = identityStore.getIdentity(); - expect(identity).toEqual({userId: 'user_id', deviceId: 'device_id'}); -}); \ No newline at end of file + expect(identity).toEqual({ userId: 'user_id', deviceId: 'device_id' }); +}); + +test('test', async () => { + const e: Record = {}; + for (const [key, value] of Object.entries(e)) { + console.log(`${key}, ${value}`); + } +}); From ace689b83f3cd4f7b0ca5d380ddf7778890f36e7 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 22 Nov 2021 11:43:40 -0800 Subject: [PATCH 05/68] complete tests --- packages/core/package.json | 5 +- packages/core/src/identityStore.ts | 69 ++++++-- packages/core/test/identityStore.test.ts | 206 ++++++++++++++++++++++- yarn.lock | 54 +++++- 4 files changed, 316 insertions(+), 18 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 7206e5b1..a9a4cd31 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,6 +27,9 @@ "url": "https://github.com/amplitude/experiment-js-client/issues" }, "homepage": "https://github.com/amplitude/experiment-js-client#readme", - "dependencies": {}, + "devDependencies": { + "@types/amplitude-js": "^8.0.2", + "amplitude-js": "^8.12.0" + }, "gitHead": "0a910f04a64dafcf37b68be45ed7dca58fdd6acf" } diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index 2dcb8e75..08f6932b 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -39,7 +39,7 @@ export class IdentityStoreImpl implements IdentityStore { editIdentity(): IdentityEditor { // eslint-disable-next-line @typescript-eslint/no-this-alias const self: IdentityStore = this; - const actingIdentity: Identity = Object.assign(this.identity); + const actingIdentity: Identity = { ...this.identity }; const editor: IdentityEditor = { setUserId: function (userId: string): IdentityEditor { actingIdentity.userId = userId; @@ -76,14 +76,14 @@ export class IdentityStoreImpl implements IdentityStore { break; case ID_OP_SET_ONCE: for (const [key, value] of Object.entries(properties)) { - if (!actingIdentity.userProperties[key]) { + if (!actingProperties[key]) { actingProperties[key] = value; } } break; case ID_OP_ADD: for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key]; + const actingValue = actingProperties[key] ?? 0; if ( typeof actingValue === 'number' && typeof value === 'number' @@ -94,17 +94,17 @@ export class IdentityStoreImpl implements IdentityStore { break; case ID_OP_APPEND: for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key]; + const actingValue = actingProperties[key] ?? []; if (Array.isArray(actingValue) && Array.isArray(value)) { - actingProperties[key] = actingValue.push(...value); + actingProperties[key] = actingValue.concat(value); } } break; case ID_OP_PREPEND: for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key]; + const actingValue = actingProperties[key] ?? []; if (Array.isArray(actingValue) && Array.isArray(value)) { - actingProperties[key] = value.push(...actingValue); + actingProperties[key] = value.concat(actingValue); } } break; @@ -126,13 +126,13 @@ export class IdentityStoreImpl implements IdentityStore { } getIdentity(): Identity { - return Object.assign(this.identity); + return { ...this.identity }; } setIdentity(identity: Identity): void { - const originalIdentity = Object.assign(this.identity); - this.identity = Object.assign(identity); - if (originalIdentity != identity) { + const originalIdentity = { ...this.identity }; + this.identity = { ...identity }; + if (!isEqual(originalIdentity, this.identity)) { this.listeners.forEach((listener) => { listener(identity); }); @@ -147,3 +147,50 @@ export class IdentityStoreImpl implements IdentityStore { this.listeners.delete(listener); } } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isEqual = (obj1: any, obj2: any): boolean => { + const primitive = ['string', 'number', 'boolean', 'undefined']; + const typeA = typeof obj1; + const typeB = typeof obj2; + if (typeA !== typeB) { + return false; + } + if (primitive.includes(typeA)) { + return obj1 === obj2; + } + //if got here - objects + if (obj1.length !== obj2.length) { + return false; + } + //check if arrays + const isArrayA = Array.isArray(obj1); + const isArrayB = Array.isArray(obj2); + if (isArrayA !== isArrayB) { + return false; + } + if (isArrayA && isArrayB) { + //arrays + for (let i = 0; i < obj1.length; i++) { + if (!isEqual(obj1[i], obj2[i])) { + return false; + } + } + } else { + //objects + const sorted1 = Object.keys(obj1).sort(); + const sorted2 = Object.keys(obj2).sort(); + if (!isEqual(sorted1, sorted2)) { + return false; + } + //compare object values + let result = true; + Object.keys(obj1).forEach((key) => { + if (!isEqual(obj1[key], obj2[key])) { + result = false; + } + }); + return result; + } + return true; +}; diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index 389f582f..99bb72ea 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -1,4 +1,6 @@ /* eslint-disable no-console */ +import amplitude from 'amplitude-js'; + import { IdentityStoreImpl } from '../src/identityStore'; test('editIdentity, setUserId setDeviceId, success', async () => { @@ -12,9 +14,203 @@ test('editIdentity, setUserId setDeviceId, success', async () => { expect(identity).toEqual({ userId: 'user_id', deviceId: 'device_id' }); }); -test('test', async () => { - const e: Record = {}; - for (const [key, value] of Object.entries(e)) { - console.log(`${key}, ${value}`); - } +test('editIdentity, setUserId setDeviceId, identity listener called', async () => { + const identityStore = new IdentityStoreImpl(); + const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; + let listenerCalled = false; + identityStore.addIdentityListener((identity) => { + expect(identity).toEqual(expectedIdentity); + listenerCalled = true; + }); + identityStore + .editIdentity() + .setUserId('user_id') + .setDeviceId('device_id') + .commit(); + expect(listenerCalled).toEqual(true); +}); + +test('setIdentity, getIdentity, success', async () => { + const identityStore = new IdentityStoreImpl(); + const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; + identityStore.setIdentity(expectedIdentity); + const identity = identityStore.getIdentity(); + expect(identity).toEqual(expectedIdentity); +}); + +test('setIdentity, identity listener called', async () => { + const identityStore = new IdentityStoreImpl(); + const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; + let listenerCalled = false; + identityStore.addIdentityListener((identity) => { + expect(identity).toEqual(expectedIdentity); + listenerCalled = true; + }); + identityStore.setIdentity(expectedIdentity); + expect(listenerCalled).toEqual(true); +}); + +test('setIdentity with unchanged identity, identity listener not called', async () => { + const identityStore = new IdentityStoreImpl(); + const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; + identityStore.setIdentity(expectedIdentity); + identityStore.addIdentityListener(() => { + fail('identity listener should not be called'); + }); + identityStore.setIdentity(expectedIdentity); +}); + +test('updateUserProperties, set', async () => { + const identityStore = new IdentityStoreImpl(); + const identify = new amplitude.Identify() + .set('string', 'string') + .set('int', 32) + .set('bool', true) + .set('double', 4.2) + .set('jsonArray', [0, 1.1, true, 'three']) + .set('jsonObject', { key: 'value' }); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + string: 'string', + int: 32, + bool: true, + double: 4.2, + jsonArray: [0, 1.1, true, 'three'], + jsonObject: { key: 'value' }, + }, + }); +}); + +test('updateUserProperties, unset', async () => { + const identityStore = new IdentityStoreImpl(); + identityStore.setIdentity({ userProperties: { key: 'value' } }); + const identify = new amplitude.Identify().unset('key'); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ userProperties: {} }); +}); + +test('updateUserProperties, setOnce', async () => { + const identityStore = new IdentityStoreImpl(); + let identify = new amplitude.Identify().setOnce('key', 'value1'); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + let identity = identityStore.getIdentity(); + expect(identity).toEqual({ userProperties: { key: 'value1' } }); + identify = new amplitude.Identify().setOnce('key', 'value2'); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + identity = identityStore.getIdentity(); + expect(identity).toEqual({ userProperties: { key: 'value1' } }); +}); + +test('updateUserProperties, add to exiting number', async () => { + const identityStore = new IdentityStoreImpl(); + const identify = new amplitude.Identify().set('int', 1).set('double', 1.1); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const add = new amplitude.Identify().add('int', 1.1).add('double', 1); + identityStore + .editIdentity() + .updateUserProperties(add['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + int: 2.1, + double: 2.1, + }, + }); +}); + +test('updateUserProperties, add to unset property', async () => { + const identityStore = new IdentityStoreImpl(); + const add = new amplitude.Identify().add('int', 1).add('double', 1.1); + identityStore + .editIdentity() + .updateUserProperties(add['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + int: 1, + double: 1.1, + }, + }); +}); + +test('updateUserProperties, append existing array', async () => { + const identityStore = new IdentityStoreImpl(); + identityStore.setIdentity({ userProperties: { key: [-3, -2, -1, 0] } }); + const identify = new amplitude.Identify().append('key', [1, 2, 3]); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + key: [-3, -2, -1, 0, 1, 2, 3], + }, + }); +}); + +test('updateUserProperties, append to unset property', async () => { + const identityStore = new IdentityStoreImpl(); + const identify = new amplitude.Identify().append('key', [1, 2, 3]); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + key: [1, 2, 3], + }, + }); +}); + +test('updateUserProperties, prepend to existing array', async () => { + const identityStore = new IdentityStoreImpl(); + identityStore.setIdentity({ userProperties: { key: [0, 1, 2, 3] } }); + const identify = new amplitude.Identify().prepend('key', [-3, -2, -1]); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + key: [-3, -2, -1, 0, 1, 2, 3], + }, + }); +}); + +test('updateUserProperties, prepend to unset array', async () => { + const identityStore = new IdentityStoreImpl(); + const identify = new amplitude.Identify().prepend('key', [1, 2, 3]); + identityStore + .editIdentity() + .updateUserProperties(identify['userPropertiesOperations']) + .commit(); + const identity = identityStore.getIdentity(); + expect(identity).toEqual({ + userProperties: { + key: [1, 2, 3], + }, + }); }); diff --git a/yarn.lock b/yarn.lock index 3219c339..5a1c3f50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,11 +3,29 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.0.3" + version "1.3.0" dependencies: base64-js "1.5.1" unfetch "4.1.0" +"@amplitude/types@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@amplitude/types/-/types-1.9.1.tgz#df614ac6d278c60c39bb49e05014cdcc7e7714e9" + integrity sha512-s10ewnlFjHjNWrUMVTPJ2YRj0eD5UoeL+d0HB01x2Ltae/jFgtHQBS/HGKYV5I+NyiaezjrrZImhAQ72Ww/+8g== + +"@amplitude/ua-parser-js@0.7.25": + version "0.7.25" + resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.25.tgz#ccbeb0bd24fca3759cfc09a3b9ba95a23ea32756" + integrity sha512-AUeO9T6vLkUNw0iYxchFBw3FylJAMv5g2sPUsS5XCulAP3TpZg9Y/QESOl+oCLGqTQYumUJZHfoQBemN22eghw== + +"@amplitude/utils@^1.0.5": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@amplitude/utils/-/utils-1.9.1.tgz#6d127b1a6f0b62c49a932ef00ecd471e554e192f" + integrity sha512-OO2w0gfs5b/dMRKh+PHhtqvb43Uf05MMUWrT3xa62P89mBtmusLvWnWycGdBu750YsnYwX8dfeZpYZ/zLH5ttQ== + dependencies: + "@amplitude/types" "^1.9.1" + tslib "^1.9.3" + "@babel/code-frame@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -3075,6 +3093,11 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-7.2.1.tgz#2ad4e844175a3738cb9e7064be5ea070b8863a1c" integrity sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA== +"@types/amplitude-js@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@types/amplitude-js/-/amplitude-js-8.0.2.tgz#576069390dd642e4b675a63c0867a52bfb96a01e" + integrity sha512-lsUZ2nfAbASU53YWNJqVmB4hFg6qRK2Dlz8FQ3vBqeDwR0OqCAi7Efowx30kvY1WgFz+dBnmRBR/D8x/NCGtwg== + "@types/aria-query@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" @@ -3796,6 +3819,16 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +amplitude-js@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/amplitude-js/-/amplitude-js-8.12.0.tgz#03828dbc6ec13d6a5cc46ffbc2438a3306dfc442" + integrity sha512-I7IS9FmRsDJJbMwdnpKMmC7DWHk/5B5fBGa5CIW2pjwEQBovrxat6ApRlomo71JCeVv4EU0nvbhisInPwfbunA== + dependencies: + "@amplitude/ua-parser-js" "0.7.25" + "@amplitude/utils" "^1.0.5" + blueimp-md5 "^2.10.0" + query-string "5" + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -4431,6 +4464,11 @@ bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +blueimp-md5@^2.10.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" + integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: version "4.11.9" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" @@ -12628,6 +12666,15 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -14727,6 +14774,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" From ecda2d4c496234ec7fb9eb128cf8cb4b327fe774 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 22 Nov 2021 12:37:44 -0800 Subject: [PATCH 06/68] add amplitude-js as dev dependencies and clean up redeclared any --- package.json | 4 +++- packages/browser/package.json | 4 ++++ packages/browser/src/integration/amplitude.ts | 5 ----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6d4c2922..40e03b43 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "ts-jest": "^26.5.4", "tslib": "^2.0.1", "typedoc": "^0.20.32", - "typescript": "^3.9.7" + "typescript": "^3.9.7", + "@types/amplitude-js": "^8.0.2", + "amplitude-js": "^8.12.0" } } diff --git a/packages/browser/package.json b/packages/browser/package.json index 76ac31d0..4ab4b26f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -31,5 +31,9 @@ "base64-js": "1.5.1", "unfetch": "4.1.0" }, + "devDependencies": { + "@types/amplitude-js": "^8.0.2", + "amplitude-js": "^8.12.0" + }, "gitHead": "0a910f04a64dafcf37b68be45ed7dca58fdd6acf" } diff --git a/packages/browser/src/integration/amplitude.ts b/packages/browser/src/integration/amplitude.ts index cf96c4f3..240236e0 100644 --- a/packages/browser/src/integration/amplitude.ts +++ b/packages/browser/src/integration/amplitude.ts @@ -6,11 +6,6 @@ import { import { ExperimentUser } from '../types/user'; import { safeGlobal } from '../util/global'; -declare global { - // eslint-disable-next-line no-var, @typescript-eslint/no-explicit-any - var amplitude: any; -} - type AmplitudeIdentify = { set(property: string, value: unknown): void; unset(property: string): void; From f3fbbfc4dbc46da8bd1d6839e17aa74ebf609820 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 23 Nov 2021 09:41:10 -0800 Subject: [PATCH 07/68] core instances be stored in global var --- packages/core/src/amplitudeCore.ts | 16 +++++++++------- packages/core/src/index.ts | 2 +- packages/core/src/util/global.ts | 4 ++++ packages/core/test/amplitudeCoreTest.ts | 10 ++++++++++ 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/util/global.ts create mode 100644 packages/core/test/amplitudeCoreTest.ts diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index 6f30815a..66d10704 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -3,16 +3,18 @@ import { AnalyticsConnectorImpl, } from './analyticsConnector'; import { IdentityStore, IdentityStoreImpl } from './identityStore'; +import { safeGlobal } from './util/global'; + +safeGlobal['amplitudeCoreInstances'] = {}; export class AmplitudeCore { identityStore: IdentityStore = new IdentityStoreImpl(); analyticsConnector: AnalyticsConnector = new AnalyticsConnectorImpl(); +} - private static instances: Record = {}; - static getInstance(instanceName: string): AmplitudeCore { - if (!AmplitudeCore.instances[instanceName]) { - AmplitudeCore.instances[instanceName] = new AmplitudeCore(); - } - return AmplitudeCore.instances[instanceName]; +export const getAmpltudeCore = (instanceName: string): AmplitudeCore => { + if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { + safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); } -} + return safeGlobal['amplitudeCoreInstances'][instanceName]; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e3f7b1ab..82f5e153 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export { AmplitudeCore } from './amplitudeCore'; +export { AmplitudeCore, getAmpltudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, diff --git a/packages/core/src/util/global.ts b/packages/core/src/util/global.ts new file mode 100644 index 00000000..e35fa2e3 --- /dev/null +++ b/packages/core/src/util/global.ts @@ -0,0 +1,4 @@ +type GlobalType = typeof globalThis | (NodeJS.Global & typeof globalThis); + +export const safeGlobal = + typeof globalThis !== 'undefined' ? globalThis : global || self; diff --git a/packages/core/test/amplitudeCoreTest.ts b/packages/core/test/amplitudeCoreTest.ts new file mode 100644 index 00000000..76f13776 --- /dev/null +++ b/packages/core/test/amplitudeCoreTest.ts @@ -0,0 +1,10 @@ +import { getAmpltudeCore } from '../src/amplitudeCore'; + +test('getAmplitudeCore returns the same instance', async () => { + const core = getAmpltudeCore('$default_instance'); + core.identityStore.setIdentity({ userId: 'userId' }); + + const core2 = getAmpltudeCore('$default_instance'); + const identity = core2.identityStore.getIdentity(); + expect(identity).toEqual({ userId: 'userId' }); +}); From 8e8638da0b4d9dd2b6262c039b38b26beb79cc92 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 10:39:35 -0800 Subject: [PATCH 08/68] commented code for consumer/producer --- packages/core/src/amplitudeCore.ts | 54 ++++++++++++++++--- ...itudeCoreTest.ts => amplitudeCore.test.ts} | 0 2 files changed, 47 insertions(+), 7 deletions(-) rename packages/core/test/{amplitudeCoreTest.ts => amplitudeCore.test.ts} (100%) diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index 66d10704..91f932e4 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -1,15 +1,55 @@ -import { - AnalyticsConnector, - AnalyticsConnectorImpl, -} from './analyticsConnector'; -import { IdentityStore, IdentityStoreImpl } from './identityStore'; +import { AnalyticsConnectorImpl } from './analyticsConnector'; +import { IdentityStoreImpl } from './identityStore'; import { safeGlobal } from './util/global'; safeGlobal['amplitudeCoreInstances'] = {}; +// export enum ComponentName { +// IdentityStore = 'IdentityStore', +// AnalyticsConnector = 'AnalyticsConnector', +// } + +// type ComponentConsumer = (component: T) => void; + export class AmplitudeCore { - identityStore: IdentityStore = new IdentityStoreImpl(); - analyticsConnector: AnalyticsConnector = new AnalyticsConnectorImpl(); + public readonly identityStore = new IdentityStoreImpl(); + public readonly analyticsConnector = new AnalyticsConnectorImpl(); + + // private components: Record = {}; + // private consumers: Record[]> = {}; + + // constructor() { + // this.produce(ComponentName.IdentityStore, new IdentityStoreImpl()); + // this.produce( + // ComponentName.AnalyticsConnector, + // new AnalyticsConnectorImpl(), + // ); + // } + + // public consume(name: ComponentName, callback: ComponentConsumer): void { + // const component = this.components[name]; + // if (component) { + // callback(component as T); + // } else { + // if (!this.consumers[name]) { + // this.consumers[name] = []; + // } + // this.consumers[name].push(callback); + // } + // } + + // public produce(name: ComponentName, component: T): void { + // if (this.components[name]) { + // return; + // } + // this.components[name] = component; + // const consumers = this.consumers[name]; + // if (consumers) { + // for (const consumer of Object.values(consumers)) { + // consumer(component as T); + // } + // } + // } } export const getAmpltudeCore = (instanceName: string): AmplitudeCore => { diff --git a/packages/core/test/amplitudeCoreTest.ts b/packages/core/test/amplitudeCore.test.ts similarity index 100% rename from packages/core/test/amplitudeCoreTest.ts rename to packages/core/test/amplitudeCore.test.ts From 0f40f3c1124f9d71ca2cac48295902532d5f7d59 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 10:45:00 -0800 Subject: [PATCH 09/68] remove provider consumer --- packages/core/src/amplitudeCore.ts | 43 ------------------------------ 1 file changed, 43 deletions(-) diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index 91f932e4..f129e040 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -4,52 +4,9 @@ import { safeGlobal } from './util/global'; safeGlobal['amplitudeCoreInstances'] = {}; -// export enum ComponentName { -// IdentityStore = 'IdentityStore', -// AnalyticsConnector = 'AnalyticsConnector', -// } - -// type ComponentConsumer = (component: T) => void; - export class AmplitudeCore { public readonly identityStore = new IdentityStoreImpl(); public readonly analyticsConnector = new AnalyticsConnectorImpl(); - - // private components: Record = {}; - // private consumers: Record[]> = {}; - - // constructor() { - // this.produce(ComponentName.IdentityStore, new IdentityStoreImpl()); - // this.produce( - // ComponentName.AnalyticsConnector, - // new AnalyticsConnectorImpl(), - // ); - // } - - // public consume(name: ComponentName, callback: ComponentConsumer): void { - // const component = this.components[name]; - // if (component) { - // callback(component as T); - // } else { - // if (!this.consumers[name]) { - // this.consumers[name] = []; - // } - // this.consumers[name].push(callback); - // } - // } - - // public produce(name: ComponentName, component: T): void { - // if (this.components[name]) { - // return; - // } - // this.components[name] = component; - // const consumers = this.consumers[name]; - // if (consumers) { - // for (const consumer of Object.values(consumers)) { - // consumer(component as T); - // } - // } - // } } export const getAmpltudeCore = (instanceName: string): AmplitudeCore => { From 623b499b0ae53ace7a688abb20da3b8a02d10798 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 15:29:46 -0800 Subject: [PATCH 10/68] feat: integrate experiment and analytics with initWithAmplitude --- package.json | 2 +- packages/browser/package.json | 3 +- packages/browser/src/factory.ts | 26 +++++++ packages/browser/src/integration/core.ts | 88 ++++++++++++++++++++++++ packages/browser/src/stubClient.ts | 3 +- packages/browser/src/types/user.ts | 10 --- packages/browser/test/client.test.ts | 7 +- packages/core/src/amplitudeCore.ts | 2 +- packages/core/src/index.ts | 2 +- packages/core/test/amplitudeCore.test.ts | 2 +- yarn.lock | 4 ++ 11 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 packages/browser/src/integration/core.ts diff --git a/package.json b/package.json index 40e03b43..b5644512 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.1", "description": "Javascript Client SDK for Amplitude Experiment", "scripts": { - "build": "yarn workspace @amplitude/experiment-js-client build", + "build": "yarn workspace @amplitude/amplitude-core build && yarn workspace @amplitude/experiment-js-client build", "lint": "lerna run lint", "test": "jest", "start": "yarn workspace browser-demo start" diff --git a/packages/browser/package.json b/packages/browser/package.json index 4ab4b26f..6e9da4cc 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -29,7 +29,8 @@ "homepage": "https://github.com/amplitude/experiment-js-client#readme", "dependencies": { "base64-js": "1.5.1", - "unfetch": "4.1.0" + "unfetch": "4.1.0", + "@amplitude/amplitude-core": "file:../core/" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index 5dd9b9c4..7e7b66b1 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -1,5 +1,8 @@ +import { getAmplitudeCore } from '@amplitude/amplitude-core'; + import { ExperimentConfig } from './config'; import { ExperimentClient } from './experimentClient'; +import { CoreAnalyticsProvider, CoreUserProvider } from './integration/core'; const instances = {}; @@ -22,10 +25,33 @@ const initialize = ( return instances[defaultInstance]; }; +const initializeWithAmplitude = ( + apiKey: string, + config?: ExperimentConfig, +): ExperimentClient => { + const core = getAmplitudeCore(defaultInstance); + if (!instances[defaultInstance]) { + if (!config.userProvider) { + config.userProvider = new CoreUserProvider(core.identityStore); + } + if (!config.analyticsProvider) { + config.analyticsProvider = new CoreAnalyticsProvider( + core.analyticsConnector, + ); + } + instances[defaultInstance] = new ExperimentClient(apiKey, config); + core.identityStore.addIdentityListener(() => { + instances[defaultInstance].fetch(); + }); + } + return instances[defaultInstance]; +}; + /** * Provides factory methods for storing singleton instances of {@link ExperimentClient} * @category Core Usage */ export const Experiment = { initialize, + initializeWithAmplitude, }; diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts new file mode 100644 index 00000000..f304854c --- /dev/null +++ b/packages/browser/src/integration/core.ts @@ -0,0 +1,88 @@ +import { + AnalyticsConnector, + AnalyticsEvent, + IdentityStore, +} from '@amplitude/amplitude-core'; + +import { ExperimentAnalyticsEvent } from '../types/analytics'; +import { + ExperimentAnalyticsProvider, + ExperimentUserProvider, +} from '../types/provider'; +import { ExperimentUser } from '../types/user'; + +type UserProperties = Record< + string, + string | number | boolean | Array +>; + +export class CoreUserProvider implements ExperimentUserProvider { + private readonly identityStore: IdentityStore; + constructor(identityStore: IdentityStore) { + this.identityStore = identityStore; + } + + async identityReady(): Promise { + const identity = this.identityStore.getIdentity(); + if (!identity.userId && !identity.deviceId) { + return new Promise((resolve) => { + const listener = () => { + resolve(); + this.identityStore.removeIdentityListener(listener); + }; + this.identityStore.addIdentityListener(listener); + }); + } + } + + getUser(): ExperimentUser { + const identity = this.identityStore.getIdentity(); + let userProperties: UserProperties = undefined; + try { + userProperties = identity.userProperties as UserProperties; + } catch { + console.warn('[Experiment] failed to cast user properties'); + } + return { + user_id: identity.userId, + device_id: identity.deviceId, + user_properties: userProperties, + // TODO: Other contextual info + }; + } +} + +export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { + private readonly analyticsConnector: AnalyticsConnector; + constructor(analyticsConnector: AnalyticsConnector) { + this.analyticsConnector = analyticsConnector; + } + + track(event: ExperimentAnalyticsEvent): void { + const analyticsEvent: AnalyticsEvent = { + eventType: event.name, + eventProperties: event.properties, + }; + this.analyticsConnector.logEvent(analyticsEvent); + } + + setUserProperty?(event: ExperimentAnalyticsEvent): void { + const analyticsEvent: AnalyticsEvent = { + eventType: '$identify', + userProperties: { + $set: { [event.userProperty]: event.variant }, + }, + }; + this.analyticsConnector.logEvent(analyticsEvent); + } + + unsetUserProperty?(event: ExperimentAnalyticsEvent): void { + const analyticsEvent: AnalyticsEvent = { + eventType: '$identify', + userProperties: { + $unset: { [event.userProperty]: event.variant }, + }, + }; + this.analyticsConnector.logEvent(analyticsEvent); + } +} diff --git a/packages/browser/src/stubClient.ts b/packages/browser/src/stubClient.ts index 7e5a1025..caa1c1fe 100644 --- a/packages/browser/src/stubClient.ts +++ b/packages/browser/src/stubClient.ts @@ -2,7 +2,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Defaults } from './config'; import { Client } from './types/client'; -import { ExperimentUser, ExperimentUserProvider } from './types/user'; +import { ExperimentUserProvider } from './types/provider'; +import { ExperimentUser } from './types/user'; import { Variant, Variants } from './types/variant'; /** diff --git a/packages/browser/src/types/user.ts b/packages/browser/src/types/user.ts index ef1efbf3..47cd31f3 100644 --- a/packages/browser/src/types/user.ts +++ b/packages/browser/src/types/user.ts @@ -87,13 +87,3 @@ export type ExperimentUser = { | Array; }; }; - -/** - * An ExperimentUserProvider injects information into the {@link ExperimentUser} - * object before sending a request to the server. This can be used to pass - * identity (deviceId and userId), or other platform specific context. - * @category User Provider - */ -export interface ExperimentUserProvider { - getUser(): ExperimentUser; -} diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 1b537182..e887a83b 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -1,7 +1,10 @@ import { ExperimentClient } from '../src/experimentClient'; -import { ExperimentAnalyticsProvider } from '../src/types/provider'; +import { + ExperimentAnalyticsProvider, + ExperimentUserProvider, +} from '../src/types/provider'; import { Source } from '../src/types/source'; -import { ExperimentUser, ExperimentUserProvider } from '../src/types/user'; +import { ExperimentUser } from '../src/types/user'; import { Variant, Variants } from '../src/types/variant'; import { randomString } from '../src/util/randomstring'; diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index f129e040..a7edbba0 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -9,7 +9,7 @@ export class AmplitudeCore { public readonly analyticsConnector = new AnalyticsConnectorImpl(); } -export const getAmpltudeCore = (instanceName: string): AmplitudeCore => { +export const getAmplitudeCore = (instanceName: string): AmplitudeCore => { if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 82f5e153..cc382442 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export { AmplitudeCore, getAmpltudeCore } from './amplitudeCore'; +export { AmplitudeCore, getAmplitudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts index 76f13776..871aec1d 100644 --- a/packages/core/test/amplitudeCore.test.ts +++ b/packages/core/test/amplitudeCore.test.ts @@ -1,4 +1,4 @@ -import { getAmpltudeCore } from '../src/amplitudeCore'; +import { getAmplitudeCore } from '../src/amplitudeCore'; test('getAmplitudeCore returns the same instance', async () => { const core = getAmpltudeCore('$default_instance'); diff --git a/yarn.lock b/yarn.lock index 5a1c3f50..a1fcb775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,9 +2,13 @@ # yarn lockfile v1 +"@amplitude/amplitude-core@file:packages/core": + version "0.0.1" + "@amplitude/experiment-js-client@file:packages/browser": version "1.3.0" dependencies: + "@amplitude/amplitude-core" "file:../../Library/Caches/Yarn/v6/npm-@amplitude-experiment-js-client-1.3.0-3bd319c3-afaf-4817-925a-9f7bdaa446cc-1638228385081/node_modules/@amplitude/core" base64-js "1.5.1" unfetch "4.1.0" From fda7083fe3ffad5d66931939a13b67f87422f800 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 15:36:48 -0800 Subject: [PATCH 11/68] rename factory method --- packages/core/src/amplitudeCore.ts | 2 +- packages/core/src/index.ts | 2 +- packages/core/test/amplitudeCore.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index f129e040..a7edbba0 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -9,7 +9,7 @@ export class AmplitudeCore { public readonly analyticsConnector = new AnalyticsConnectorImpl(); } -export const getAmpltudeCore = (instanceName: string): AmplitudeCore => { +export const getAmplitudeCore = (instanceName: string): AmplitudeCore => { if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 82f5e153..cc382442 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export { AmplitudeCore, getAmpltudeCore } from './amplitudeCore'; +export { AmplitudeCore, getAmplitudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts index 76f13776..871aec1d 100644 --- a/packages/core/test/amplitudeCore.test.ts +++ b/packages/core/test/amplitudeCore.test.ts @@ -1,4 +1,4 @@ -import { getAmpltudeCore } from '../src/amplitudeCore'; +import { getAmplitudeCore } from '../src/amplitudeCore'; test('getAmplitudeCore returns the same instance', async () => { const core = getAmpltudeCore('$default_instance'); From c8bd5af84b9fd59fdcb2408df41c6883dc56ceec Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 15:50:15 -0800 Subject: [PATCH 12/68] update comment --- packages/browser/src/integration/amplitude.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/integration/amplitude.ts b/packages/browser/src/integration/amplitude.ts index 240236e0..8a36f03e 100644 --- a/packages/browser/src/integration/amplitude.ts +++ b/packages/browser/src/integration/amplitude.ts @@ -38,9 +38,10 @@ type AmplitudeUAParser = { }; /** - * An AmplitudeUserProvider injects information from the Amplitude SDK into - * the {@link ExperimentUser} object before sending a request to the server. - * @category Context Provider + * DEPRECATED: This implementation is now deprecated. + * + * Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * integration with the amplitude analytics SDK. */ export class AmplitudeUserProvider implements ExperimentUserProvider { private amplitudeInstance: AmplitudeInstance; @@ -75,8 +76,10 @@ export class AmplitudeUserProvider implements ExperimentUserProvider { } /** - * Provides a tracking implementation for standard experiment events generated - * by the client (e.g. exposure). + * DEPRECATED: This implementation is now deprecated. + * + * Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * integration with the amplitude analytics SDK. */ export class AmplitudeAnalyticsProvider implements ExperimentAnalyticsProvider { private amplitudeInstance: AmplitudeInstance; From 2b7ba136680a248e0a9670b09a0881265f341ae7 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 15:53:08 -0800 Subject: [PATCH 13/68] update test --- packages/core/test/amplitudeCore.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts index 871aec1d..46607ca8 100644 --- a/packages/core/test/amplitudeCore.test.ts +++ b/packages/core/test/amplitudeCore.test.ts @@ -1,10 +1,10 @@ import { getAmplitudeCore } from '../src/amplitudeCore'; test('getAmplitudeCore returns the same instance', async () => { - const core = getAmpltudeCore('$default_instance'); + const core = getAmplitudeCore('$default_instance'); core.identityStore.setIdentity({ userId: 'userId' }); - const core2 = getAmpltudeCore('$default_instance'); + const core2 = getAmplitudeCore('$default_instance'); const identity = core2.identityStore.getIdentity(); expect(identity).toEqual({ userId: 'userId' }); }); From 27bc678f7605f0dce293a021ea43eea0c8c5c09d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 15:53:48 -0800 Subject: [PATCH 14/68] update tests --- packages/core/test/amplitudeCore.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts index 871aec1d..46607ca8 100644 --- a/packages/core/test/amplitudeCore.test.ts +++ b/packages/core/test/amplitudeCore.test.ts @@ -1,10 +1,10 @@ import { getAmplitudeCore } from '../src/amplitudeCore'; test('getAmplitudeCore returns the same instance', async () => { - const core = getAmpltudeCore('$default_instance'); + const core = getAmplitudeCore('$default_instance'); core.identityStore.setIdentity({ userId: 'userId' }); - const core2 = getAmpltudeCore('$default_instance'); + const core2 = getAmplitudeCore('$default_instance'); const identity = core2.identityStore.getIdentity(); expect(identity).toEqual({ userId: 'userId' }); }); From b0c2a90bc42c29f6dab4303a0dcedda7caa82f1c Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 16:15:39 -0800 Subject: [PATCH 15/68] use static init --- packages/core/src/amplitudeCore.ts | 12 ++++++------ packages/core/test/amplitudeCore.test.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index a7edbba0..f29a14b2 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -7,11 +7,11 @@ safeGlobal['amplitudeCoreInstances'] = {}; export class AmplitudeCore { public readonly identityStore = new IdentityStoreImpl(); public readonly analyticsConnector = new AnalyticsConnectorImpl(); -} -export const getAmplitudeCore = (instanceName: string): AmplitudeCore => { - if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { - safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); + static getInstance(instanceName: string): AmplitudeCore { + if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { + safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); + } + return safeGlobal['amplitudeCoreInstances'][instanceName]; } - return safeGlobal['amplitudeCoreInstances'][instanceName]; -}; +} diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts index 46607ca8..9736f375 100644 --- a/packages/core/test/amplitudeCore.test.ts +++ b/packages/core/test/amplitudeCore.test.ts @@ -1,10 +1,10 @@ -import { getAmplitudeCore } from '../src/amplitudeCore'; +import { AmplitudeCore } from '../src/amplitudeCore'; test('getAmplitudeCore returns the same instance', async () => { - const core = getAmplitudeCore('$default_instance'); + const core = AmplitudeCore.getInstance('$default_instance'); core.identityStore.setIdentity({ userId: 'userId' }); - const core2 = getAmplitudeCore('$default_instance'); + const core2 = AmplitudeCore.getInstance('$default_instance'); const identity = core2.identityStore.getIdentity(); expect(identity).toEqual({ userId: 'userId' }); }); From 189282b2bc3e5857677c29973ffcfcb179c941de Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 16:28:47 -0800 Subject: [PATCH 16/68] update core --- packages/browser/src/factory.ts | 4 ++-- packages/core/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index 7e7b66b1..db05b22b 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -1,4 +1,4 @@ -import { getAmplitudeCore } from '@amplitude/amplitude-core'; +import { AmplitudeCore } from '@amplitude/amplitude-core'; import { ExperimentConfig } from './config'; import { ExperimentClient } from './experimentClient'; @@ -29,7 +29,7 @@ const initializeWithAmplitude = ( apiKey: string, config?: ExperimentConfig, ): ExperimentClient => { - const core = getAmplitudeCore(defaultInstance); + const core = AmplitudeCore.getInstance(defaultInstance); if (!instances[defaultInstance]) { if (!config.userProvider) { config.userProvider = new CoreUserProvider(core.identityStore); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cc382442..e3f7b1ab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export { AmplitudeCore, getAmplitudeCore } from './amplitudeCore'; +export { AmplitudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, From 363aa5ff2707dfabccfbc093437f8c7525abce4d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 29 Nov 2021 16:31:54 -0800 Subject: [PATCH 17/68] fix build --- package.json | 2 +- packages/core/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40e03b43..b5644512 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.1", "description": "Javascript Client SDK for Amplitude Experiment", "scripts": { - "build": "yarn workspace @amplitude/experiment-js-client build", + "build": "yarn workspace @amplitude/amplitude-core build && yarn workspace @amplitude/experiment-js-client build", "lint": "lerna run lint", "test": "jest", "start": "yarn workspace browser-demo start" diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cc382442..e3f7b1ab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -export { AmplitudeCore, getAmplitudeCore } from './amplitudeCore'; +export { AmplitudeCore } from './amplitudeCore'; export { AnalyticsConnector, AnalyticsEvent, From 6c24e307d7fdc256de6416adab7f16e25baef7f9 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 30 Nov 2021 15:10:17 -0800 Subject: [PATCH 18/68] minor change --- .gitignore | 3 +++ packages/browser/package.json | 2 +- packages/core/rollup.config.js | 2 +- yarn.lock | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2b154bac..a73eebf9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ dist/ # MacOS .DS_Store + +# WebStorm +.idea diff --git a/packages/browser/package.json b/packages/browser/package.json index 6e9da4cc..c0c35a34 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "file:../core/" + "@amplitude/amplitude-core": "file:../core" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js index 03960f3e..63a1f5c9 100644 --- a/packages/core/rollup.config.js +++ b/packages/core/rollup.config.js @@ -11,7 +11,7 @@ const browserConfig = { input: 'src/index.ts', output: { dir: 'dist', - entryFileNames: 'experiment.umd.js', + entryFileNames: 'core.umd.js', exports: 'named', format: 'umd', name: 'Experiment', diff --git a/yarn.lock b/yarn.lock index a1fcb775..79c8aed4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,7 +8,7 @@ "@amplitude/experiment-js-client@file:packages/browser": version "1.3.0" dependencies: - "@amplitude/amplitude-core" "file:../../Library/Caches/Yarn/v6/npm-@amplitude-experiment-js-client-1.3.0-3bd319c3-afaf-4817-925a-9f7bdaa446cc-1638228385081/node_modules/@amplitude/core" + "@amplitude/amplitude-core" "file:../../Library/Caches/Yarn/v6/npm-@amplitude-experiment-js-client-1.3.0-9a761fc7-8814-428d-aaf3-d803034905e6-1638307643567/node_modules/@amplitude/core" base64-js "1.5.1" unfetch "4.1.0" From 9bb2b924b932bc98b421c0cf5641fa3e71bd0912 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 30 Nov 2021 15:11:53 -0800 Subject: [PATCH 19/68] update rollup --- .gitignore | 2 ++ packages/core/rollup.config.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2b154bac..865599f1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ dist/ # MacOS .DS_Store + +.idea diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js index 03960f3e..63a1f5c9 100644 --- a/packages/core/rollup.config.js +++ b/packages/core/rollup.config.js @@ -11,7 +11,7 @@ const browserConfig = { input: 'src/index.ts', output: { dir: 'dist', - entryFileNames: 'experiment.umd.js', + entryFileNames: 'core.umd.js', exports: 'named', format: 'umd', name: 'Experiment', From a90cd88e8595317a53a68dffcd71b898d362643b Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 1 Dec 2021 14:15:17 -0800 Subject: [PATCH 20/68] remove unecessary dev deps --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index b5644512..f2f8fdd5 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,6 @@ "ts-jest": "^26.5.4", "tslib": "^2.0.1", "typedoc": "^0.20.32", - "typescript": "^3.9.7", - "@types/amplitude-js": "^8.0.2", - "amplitude-js": "^8.12.0" + "typescript": "^3.9.7" } } From fbd0f17f49dbdedc1ac8df839173dd79d5ec5e6d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 10:13:25 -0800 Subject: [PATCH 21/68] 1.4.0-alpha.0 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index c0c35a34..2edef3f8 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.3.0", + "version": "1.4.0-alpha.0", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From 60d1e9f23e44c4d5681be668a0e12aadaec20eb4 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 10:15:02 -0800 Subject: [PATCH 22/68] use real core dependency --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 2edef3f8..9ca22be7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "file:../core" + "@amplitude/amplitude-core": "0.0.1" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", From c52e337d65c4c7f8ca222766061e568a6967956d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 15:11:09 -0800 Subject: [PATCH 23/68] fix crash in initWithAmplitude --- packages/browser/package.json | 2 +- packages/browser/src/factory.ts | 1 + yarn.lock | 7 ++----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 9ca22be7..2edf9b15 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.0", + "version": "1.4.0-alpha.1", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index db05b22b..d3c52894 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -31,6 +31,7 @@ const initializeWithAmplitude = ( ): ExperimentClient => { const core = AmplitudeCore.getInstance(defaultInstance); if (!instances[defaultInstance]) { + config = config || {}; if (!config.userProvider) { config.userProvider = new CoreUserProvider(core.identityStore); } diff --git a/yarn.lock b/yarn.lock index 79c8aed4..00d0b6dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,10 @@ # yarn lockfile v1 -"@amplitude/amplitude-core@file:packages/core": - version "0.0.1" - "@amplitude/experiment-js-client@file:packages/browser": - version "1.3.0" + version "1.4.0-alpha.0" dependencies: - "@amplitude/amplitude-core" "file:../../Library/Caches/Yarn/v6/npm-@amplitude-experiment-js-client-1.3.0-9a761fc7-8814-428d-aaf3-d803034905e6-1638307643567/node_modules/@amplitude/core" + "@amplitude/amplitude-core" "0.0.1" base64-js "1.5.1" unfetch "4.1.0" From 1ff84d45c40b3cc21d07ca98bdd6da3460955873 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 16:13:20 -0800 Subject: [PATCH 24/68] only track exposure changes or once per session --- packages/browser/src/integration/core.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index f304854c..c2525886 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -54,35 +54,55 @@ export class CoreUserProvider implements ExperimentUserProvider { export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { private readonly analyticsConnector: AnalyticsConnector; + + // In memory record of flagKey and variant value to in order to only set + // user properties and track an exposure event once per session unless the + // variant value changes + private readonly exposures: Record = {}; + constructor(analyticsConnector: AnalyticsConnector) { this.analyticsConnector = analyticsConnector; } track(event: ExperimentAnalyticsEvent): void { + if (this.hasAlreadyBeenExposedTo(event.key, event.variant.value)) { + return; + } else { + this.exposures[event.key] = event.variant.value; + } const analyticsEvent: AnalyticsEvent = { eventType: event.name, eventProperties: event.properties, + userProperties: { $set: { [event.userProperty]: event.variant.value } }, }; this.analyticsConnector.logEvent(analyticsEvent); } setUserProperty?(event: ExperimentAnalyticsEvent): void { + if (this.hasAlreadyBeenExposedTo(event.key, event.variant.value)) { + return; + } const analyticsEvent: AnalyticsEvent = { eventType: '$identify', userProperties: { - $set: { [event.userProperty]: event.variant }, + $set: { [event.userProperty]: event.variant.value }, }, }; this.analyticsConnector.logEvent(analyticsEvent); } unsetUserProperty?(event: ExperimentAnalyticsEvent): void { + delete this.exposures[event.key]; const analyticsEvent: AnalyticsEvent = { eventType: '$identify', userProperties: { - $unset: { [event.userProperty]: event.variant }, + $unset: { [event.userProperty]: event.variant.value }, }, }; this.analyticsConnector.logEvent(analyticsEvent); } + + private hasAlreadyBeenExposedTo(flagKey: string, value: string): boolean { + return this.exposures && this.exposures[flagKey] == value; + } } From c816699382c004e96be3e8eb230107bc3ee1fced Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 17:33:06 -0800 Subject: [PATCH 25/68] await ready when adding user context from core user provider --- packages/browser/src/experimentClient.ts | 31 ++++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index 89861bd5..cf5e4965 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -6,6 +6,7 @@ import { version as PACKAGE_VERSION } from '../package.json'; import { ExperimentConfig, Defaults } from './config'; +import { CoreUserProvider } from './integration/core'; import { LocalStorage } from './storage/localStorage'; import { FetchHttpClient } from './transport/http'; import { exposureEvent } from './types/analytics'; @@ -134,19 +135,20 @@ export class ExperimentClient implements Client { if (isFallback(source) || !variant?.value) { // fallbacks indicate not being allocated into an experiment, so // we can unset the property - this.config.analyticsProvider?.unsetUserProperty?.( - exposureEvent(this.addContext(this.getUser()), key, variant, source), - ); + this.addContext(this.getUser()).then((user) => { + this.config.analyticsProvider?.unsetUserProperty?.( + exposureEvent(user, key, variant, source), + ); + }); } else if (variant?.value) { - // only track when there's a value for a non fallback variant - const event = exposureEvent( - this.addContext(this.getUser()), - key, - variant, - source, - ); - this.config.analyticsProvider?.setUserProperty?.(event); - this.config.analyticsProvider?.track(event); + // fallbacks indicate not being allocated into an experiment, so + // we can unset the property + this.addContext(this.getUser()).then((user) => { + // only track when there's a value for a non fallback variant + const event = exposureEvent(user, key, variant, source); + this.config.analyticsProvider?.setUserProperty?.(event); + this.config.analyticsProvider?.track(event); + }); } this.debug(`[Experiment] variant for ${key} is ${variant.value}`); @@ -402,7 +404,10 @@ export class ExperimentClient implements Client { } } - private addContext(user: ExperimentUser) { + private async addContext(user: ExperimentUser): Promise { + if (this.userProvider instanceof CoreUserProvider) { + await this.userProvider.identityReady(); + } return { library: `experiment-js-client/${PACKAGE_VERSION}`, ...this.userProvider?.getUser(), From 2df2a812be54ddcc3259122c5728a0edfcd44dd0 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 6 Dec 2021 17:44:28 -0800 Subject: [PATCH 26/68] await context in doFetch --- packages/browser/package.json | 2 +- packages/browser/src/experimentClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 2edf9b15..68bb462e 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.1", + "version": "1.4.0-alpha.2", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index cf5e4965..f99b1dfb 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -337,7 +337,7 @@ export class ExperimentClient implements Client { user: ExperimentUser, timeoutMillis: number, ): Promise { - const userContext = this.addContext(user); + const userContext = await this.addContext(user); const encodedContext = urlSafeBase64Encode(JSON.stringify(userContext)); let queryString = ''; if (this.config.debug) { From a8f02f10d7b656cb0f768b3af5c3dc8325b3de38 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 14:53:28 -0800 Subject: [PATCH 27/68] merge provided and explicit user properties --- packages/browser/src/experimentClient.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index f99b1dfb..adb560e2 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -408,10 +408,16 @@ export class ExperimentClient implements Client { if (this.userProvider instanceof CoreUserProvider) { await this.userProvider.identityReady(); } + const providedUser = this.userProvider?.getUser(); + const mergedUserProperties = { + ...user?.user_properties, + ...providedUser?.user_properties, + }; return { library: `experiment-js-client/${PACKAGE_VERSION}`, ...this.userProvider?.getUser(), ...user, + user_properties: mergedUserProperties, }; } From 2558e50f28e9b4dba164f03f6940da30cad840cb Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 15:16:14 -0800 Subject: [PATCH 28/68] 1.4.0-alpha.3 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 68bb462e..1edd5068 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.2", + "version": "1.4.0-alpha.3", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From ff60d84aec0dcce91ec76a3bba441ffd1f7f8842 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:13:00 -0800 Subject: [PATCH 29/68] copy user properties when editing identity to call listener --- packages/core/src/identityStore.ts | 6 ++++- packages/core/test/identityStore.test.ts | 29 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index 08f6932b..eb6264ee 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -39,7 +39,11 @@ export class IdentityStoreImpl implements IdentityStore { editIdentity(): IdentityEditor { // eslint-disable-next-line @typescript-eslint/no-this-alias const self: IdentityStore = this; - const actingIdentity: Identity = { ...this.identity }; + const actingUserProperties = { ...this.identity.userProperties }; + const actingIdentity: Identity = { + ...this.identity, + userProperties: actingUserProperties, + }; const editor: IdentityEditor = { setUserId: function (userId: string): IdentityEditor { actingIdentity.userId = userId; diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index 99bb72ea..199140a7 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -30,6 +30,35 @@ test('editIdentity, setUserId setDeviceId, identity listener called', async () = expect(listenerCalled).toEqual(true); }); +test('editIdentity, updateUserProperties, identity listener called', async () => { + const identityStore = new IdentityStoreImpl(); + let listenerCalled = false; + identityStore.addIdentityListener(() => { + listenerCalled = true; + }); + + identityStore + .editIdentity() + .setUserId('user_id') + .setDeviceId('device_id') + .commit(); + expect(listenerCalled).toEqual(true); + + listenerCalled = false; + identityStore + .editIdentity() + .updateUserProperties({ $set: { test: 'test' } }) + .commit(); + expect(listenerCalled).toEqual(true); + + listenerCalled = false; + identityStore + .editIdentity() + .updateUserProperties({ $set: { test: 'test2' } }) + .commit(); + expect(listenerCalled).toEqual(true); +}); + test('setIdentity, getIdentity, success', async () => { const identityStore = new IdentityStoreImpl(); const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; From b3c089758fee120479ee8d168bcb68b925220d8a Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:15:14 -0800 Subject: [PATCH 30/68] 0.0.2 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index a9a4cd31..7f055cd0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/amplitude-core", - "version": "0.0.1", + "version": "0.0.2", "description": "Core package for Amplitide SDKs", "main": "dist/core.umd.js", "types": "dist/types/src/index.d.ts", From c585cb92d219c2d6aed5fc4bb1438b2d33eb86a4 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:19:41 -0800 Subject: [PATCH 31/68] update core dependency --- packages/browser/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 1edd5068..b602a993 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "0.0.1" + "@amplitude/amplitude-core": "0.0.2" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/yarn.lock b/yarn.lock index 00d0b6dc..e6402ae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0-alpha.0" + version "1.4.0-alpha.3" dependencies: - "@amplitude/amplitude-core" "0.0.1" + "@amplitude/amplitude-core" "0.0.2" base64-js "1.5.1" unfetch "4.1.0" From c8069d0e5977aee4952b9435fc4216f34753c91f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:19:56 -0800 Subject: [PATCH 32/68] 1.4.0-alpha.4 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index b602a993..d761cf6b 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.3", + "version": "1.4.0-alpha.4", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From 88888dd0984a54e668046d11d080a116bbc52fc4 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:47:22 -0800 Subject: [PATCH 33/68] 1.4.0-alpha.5 --- packages/browser/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index d761cf6b..b794ce12 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.4", + "version": "1.4.0-alpha.5", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/yarn.lock b/yarn.lock index e6402ae2..68e93caa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,7 +3,7 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0-alpha.3" + version "1.4.0-alpha.4" dependencies: "@amplitude/amplitude-core" "0.0.2" base64-js "1.5.1" From 91f6987d8d9de32f0e3c15f1c2738a4ce5becac5 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 16:53:05 -0800 Subject: [PATCH 34/68] 0.0.3 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 7f055cd0..70544884 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/amplitude-core", - "version": "0.0.2", + "version": "0.0.3", "description": "Core package for Amplitide SDKs", "main": "dist/core.umd.js", "types": "dist/types/src/index.d.ts", From 8fb0ebe639cbeaa05dad25354be37f106a00e758 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 17:11:55 -0800 Subject: [PATCH 35/68] alpha 5 --- packages/browser/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index b794ce12..d15682d5 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "0.0.2" + "@amplitude/amplitude-core": "0.0.3" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/yarn.lock b/yarn.lock index 68e93caa..034006ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0-alpha.4" + version "1.4.0-alpha.5" dependencies: - "@amplitude/amplitude-core" "0.0.2" + "@amplitude/amplitude-core" "0.0.3" base64-js "1.5.1" unfetch "4.1.0" From e6e9e52d72b7521ae05761c1af3cc145d3fd3d87 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 7 Dec 2021 17:12:24 -0800 Subject: [PATCH 36/68] alpha 6 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index d15682d5..316e100a 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.5", + "version": "1.4.0-alpha.6", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From 40bcd301764b4985451cf3d9c08ccd38505babed Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 10 Dec 2021 15:13:49 -0800 Subject: [PATCH 37/68] track unsets to not spam useless identify calls --- packages/browser/package.json | 2 +- packages/browser/src/integration/core.ts | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 316e100a..44f937e5 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.6", + "version": "1.4.0-alpha.7", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index c2525886..2d189d61 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -58,17 +58,19 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { // In memory record of flagKey and variant value to in order to only set // user properties and track an exposure event once per session unless the // variant value changes - private readonly exposures: Record = {}; + private readonly setProperties: Record = {}; + private readonly unsetProperties: Record = {}; constructor(analyticsConnector: AnalyticsConnector) { this.analyticsConnector = analyticsConnector; } track(event: ExperimentAnalyticsEvent): void { - if (this.hasAlreadyBeenExposedTo(event.key, event.variant.value)) { + if (this.setProperties[event.key] == event.variant.value) { return; } else { - this.exposures[event.key] = event.variant.value; + this.setProperties[event.key] = event.variant.value; + delete this.unsetProperties[event.key]; } const analyticsEvent: AnalyticsEvent = { eventType: event.name, @@ -79,7 +81,7 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { } setUserProperty?(event: ExperimentAnalyticsEvent): void { - if (this.hasAlreadyBeenExposedTo(event.key, event.variant.value)) { + if (this.setProperties[event.key] == event.variant.value) { return; } const analyticsEvent: AnalyticsEvent = { @@ -92,7 +94,12 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { } unsetUserProperty?(event: ExperimentAnalyticsEvent): void { - delete this.exposures[event.key]; + if (this.unsetProperties[event.key]) { + return; + } else { + this.unsetProperties[event.key] = 'unset'; + delete this.setProperties[event.key]; + } const analyticsEvent: AnalyticsEvent = { eventType: '$identify', userProperties: { @@ -101,8 +108,4 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { }; this.analyticsConnector.logEvent(analyticsEvent); } - - private hasAlreadyBeenExposedTo(flagKey: string, value: string): boolean { - return this.exposures && this.exposures[flagKey] == value; - } } From 4932cb0edb90c2d05116bc15dac0db6e546f7904 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 10 Dec 2021 15:24:30 -0800 Subject: [PATCH 38/68] fix tests --- packages/browser/test/client.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index e887a83b..46f0c0d2 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -239,11 +239,15 @@ test('ExperimentClient.variant, with analytics provider, exposure tracked, unset const spySet = jest.spyOn(analyticsProvider, 'setUserProperty'); const spyUnset = jest.spyOn(analyticsProvider, 'unsetUserProperty'); const client = new ExperimentClient(API_KEY, { + debug: true, analyticsProvider: analyticsProvider, }); await client.fetch(testUser); client.variant(serverKey); + // analytics provider call is asynchronous + await delay(100); + expect(spySet).toBeCalledTimes(1); expect(spyTrack).toBeCalledTimes(1); @@ -290,6 +294,10 @@ test('ExperimentClient.variant, with analytics provider, exposure not tracked on }); client.variant(initialKey); client.variant(unknownKey); + + // analytics provider call is asynchronous + await delay(100); + expect(spyTrack).toHaveBeenCalledTimes(0); expect(spySet).toHaveBeenCalledTimes(0); expect(spyUnset).toHaveBeenCalledTimes(2); From baa3e3ed3206387d7382d0ec6c367fbbf75393f7 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 09:28:13 -0800 Subject: [PATCH 39/68] add test for unset only called once --- packages/browser/test/client.test.ts | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 46f0c0d2..c5b471b1 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -1,3 +1,6 @@ +import { AnalyticsConnectorImpl } from '@amplitude/amplitude-core/src/analyticsConnector'; +import { CoreAnalyticsProvider } from 'src/integration/core'; + import { ExperimentClient } from '../src/experimentClient'; import { ExperimentAnalyticsProvider, @@ -176,7 +179,7 @@ test('ExperimentClient.fetch, initial variants source, prefer initial', async () }); /** - * Test that fetch with an explicit user arguement will set the user within the + * Test that fetch with an explicit user argument will set the user within the * client, and calling setUser() after will overwrite the user. */ test('ExperimentClient.fetch, sets user, setUser overrides', async () => { @@ -229,6 +232,30 @@ class TestAnalyticsProvider implements ExperimentAnalyticsProvider { } } +test('ExperimentClient.variant, with analytics provider, unset called only once per key', async () => { + const analyticsConnector = new AnalyticsConnectorImpl(); + const analyticsProvider = new CoreAnalyticsProvider(analyticsConnector); + const unsetSpy = jest.spyOn(analyticsProvider, 'unsetUserProperty'); + let eventCount = 0; + analyticsConnector.setEventReceiver(() => { + eventCount++; + }); + const client = new ExperimentClient(API_KEY, { + debug: true, + analyticsProvider: analyticsProvider, + }); + await client.fetch(testUser); + for (let i = 0; i < 100; i++) { + client.variant('key-that-does-not-exist'); + } + + // analytics provider call is asynchronous + await delay(1000); + + expect(unsetSpy).toBeCalledTimes(100); + expect(eventCount).toEqual(1); +}); + /** * Configure a client with an analytics provider which checks that a valid * exposure event is tracked when the client's variant function is called. @@ -246,7 +273,7 @@ test('ExperimentClient.variant, with analytics provider, exposure tracked, unset client.variant(serverKey); // analytics provider call is asynchronous - await delay(100); + await delay(1000); expect(spySet).toBeCalledTimes(1); expect(spyTrack).toBeCalledTimes(1); @@ -296,7 +323,7 @@ test('ExperimentClient.variant, with analytics provider, exposure not tracked on client.variant(unknownKey); // analytics provider call is asynchronous - await delay(100); + await delay(1000); expect(spyTrack).toHaveBeenCalledTimes(0); expect(spySet).toHaveBeenCalledTimes(0); From ca7f2ff409a755d7969b34d7381b83eacf0c9016 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 10:17:55 -0800 Subject: [PATCH 40/68] fix core tests --- packages/core/src/identityStore.ts | 5 ++--- packages/core/test/identityStore.test.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index eb6264ee..f7efd15a 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -33,7 +33,7 @@ export interface IdentityEditor { } export class IdentityStoreImpl implements IdentityStore { - private identity: Identity = {}; + private identity: Identity = { userProperties: {} }; private listeners = new Set(); editIdentity(): IdentityEditor { @@ -44,7 +44,7 @@ export class IdentityStoreImpl implements IdentityStore { ...this.identity, userProperties: actingUserProperties, }; - const editor: IdentityEditor = { + return { setUserId: function (userId: string): IdentityEditor { actingIdentity.userId = userId; return this; @@ -126,7 +126,6 @@ export class IdentityStoreImpl implements IdentityStore { return this; }, }; - return editor; } getIdentity(): Identity { diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index 199140a7..24ebd371 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -11,12 +11,20 @@ test('editIdentity, setUserId setDeviceId, success', async () => { .setDeviceId('device_id') .commit(); const identity = identityStore.getIdentity(); - expect(identity).toEqual({ userId: 'user_id', deviceId: 'device_id' }); + expect(identity).toEqual({ + userId: 'user_id', + deviceId: 'device_id', + userProperties: {}, + }); }); test('editIdentity, setUserId setDeviceId, identity listener called', async () => { const identityStore = new IdentityStoreImpl(); - const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; + const expectedIdentity = { + userId: 'user_id', + deviceId: 'device_id', + userProperties: {}, + }; let listenerCalled = false; identityStore.addIdentityListener((identity) => { expect(identity).toEqual(expectedIdentity); From e46e8e8280987938d1e37f563efb6c17e56ba0c8 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 10:25:11 -0800 Subject: [PATCH 41/68] remove debug from client --- packages/browser/test/client.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index c5b471b1..59bef99f 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -241,7 +241,6 @@ test('ExperimentClient.variant, with analytics provider, unset called only once eventCount++; }); const client = new ExperimentClient(API_KEY, { - debug: true, analyticsProvider: analyticsProvider, }); await client.fetch(testUser); From cedabf559c5fb7037e8a90f743e6cfe8befb24d2 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 10:31:13 -0800 Subject: [PATCH 42/68] alpha 8 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 44f937e5..2a7c9251 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.7", + "version": "1.4.0-alpha.8", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From cb0f42ccec124139f95b02abaf4d52ad2615b04f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 13:46:53 -0800 Subject: [PATCH 43/68] use @deprecated comment --- packages/browser/src/integration/amplitude.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/browser/src/integration/amplitude.ts b/packages/browser/src/integration/amplitude.ts index 8a36f03e..986f2ea3 100644 --- a/packages/browser/src/integration/amplitude.ts +++ b/packages/browser/src/integration/amplitude.ts @@ -38,9 +38,7 @@ type AmplitudeUAParser = { }; /** - * DEPRECATED: This implementation is now deprecated. - * - * Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * @deprecated Update your version of the amplitude analytics SDK to X.X.X+ and for seamless * integration with the amplitude analytics SDK. */ export class AmplitudeUserProvider implements ExperimentUserProvider { @@ -76,9 +74,7 @@ export class AmplitudeUserProvider implements ExperimentUserProvider { } /** - * DEPRECATED: This implementation is now deprecated. - * - * Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * @deprecated Update your version of the amplitude analytics SDK to X.X.X+ and for seamless * integration with the amplitude analytics SDK. */ export class AmplitudeAnalyticsProvider implements ExperimentAnalyticsProvider { From 5f04d682607f085505936b6d1829cf47c9e14664 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 13 Dec 2021 14:41:01 -0800 Subject: [PATCH 44/68] add max queue size --- packages/core/src/analyticsConnector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/analyticsConnector.ts b/packages/core/src/analyticsConnector.ts index d52dd6af..04f92536 100644 --- a/packages/core/src/analyticsConnector.ts +++ b/packages/core/src/analyticsConnector.ts @@ -17,7 +17,9 @@ export class AnalyticsConnectorImpl implements AnalyticsConnector { logEvent(event: AnalyticsEvent): void { if (!this.receiver) { - this.queue.push(event); + if (this.queue.length < 512) { + this.queue.push(event); + } } else { this.receiver(event); } From 7c7af70441dd4e1b3a39af57a46742e54d7242a1 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 14 Dec 2021 14:41:11 -0800 Subject: [PATCH 45/68] support multiple instances; alpha 9 --- packages/browser/package.json | 2 +- packages/browser/src/config.ts | 9 +++++ packages/browser/src/experimentClient.ts | 5 +-- packages/browser/src/factory.ts | 45 ++++++++++++++++-------- packages/browser/src/integration/core.ts | 2 +- packages/browser/test/factory.test.ts | 32 +++++++++++++++++ yarn.lock | 2 +- 7 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 packages/browser/test/factory.test.ts diff --git a/packages/browser/package.json b/packages/browser/package.json index 2a7c9251..cd30b773 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.8", + "version": "1.4.0-alpha.9", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/packages/browser/src/config.ts b/packages/browser/src/config.ts index 03424d9d..55276f8a 100644 --- a/packages/browser/src/config.ts +++ b/packages/browser/src/config.ts @@ -15,6 +15,13 @@ export interface ExperimentConfig { */ debug?: boolean; + /** + * The name of the instance being initialized. Used for initializing separate + * instances of experiment or linking the experiment SDK to a specific + * instance of the amplitude analytics SDK. + */ + instanceName?: string; + /** * The default fallback variant for all {@link ExperimentClient.variant} * calls. @@ -72,6 +79,7 @@ export interface ExperimentConfig { | **Option** | **Default** | |------------------|-----------------------------------| | **debug** | `false` | + | **instanceName** | `$default_instance` | | **fallbackVariant** | `null` | | **initialVariants** | `null` | | **source** | `Source.LocalStorage` | @@ -86,6 +94,7 @@ export interface ExperimentConfig { */ export const Defaults: ExperimentConfig = { debug: false, + instanceName: '$default_instance', fallbackVariant: {}, initialVariants: {}, source: Source.LocalStorage, diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index adb560e2..907b62b7 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -30,9 +30,6 @@ const fetchBackoffMinMillis = 500; const fetchBackoffMaxMillis = 10000; const fetchBackoffScalar = 1.5; -// TODO this is defined twice, figure something better out. -const defaultInstance = '$default_instance'; - /** * The default {@link Client} used to fetch variations from Experiment's * servers. @@ -69,7 +66,7 @@ export class ExperimentClient implements Client { this.userProvider = this.config.userProvider; } this.httpClient = FetchHttpClient; - this.storage = new LocalStorage(defaultInstance, apiKey); + this.storage = new LocalStorage(this.config.instanceName, apiKey); this.storage.load(); } diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index d3c52894..47c243ac 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -1,16 +1,14 @@ import { AmplitudeCore } from '@amplitude/amplitude-core'; -import { ExperimentConfig } from './config'; +import { Defaults, ExperimentConfig } from './config'; import { ExperimentClient } from './experimentClient'; import { CoreAnalyticsProvider, CoreUserProvider } from './integration/core'; const instances = {}; -// TODO this is defined twice, figure something better out. -const defaultInstance = '$default_instance'; - /** - * Initializes a singleton {@link ExperimentClient} identified by the api-key. + * Initializes a singleton {@link ExperimentClient} identified by the configured + * instance name. * * @param apiKey The environment API Key * @param config See {@link ExperimentConfig} for config options @@ -19,18 +17,35 @@ const initialize = ( apiKey: string, config?: ExperimentConfig, ): ExperimentClient => { - if (!instances[defaultInstance]) { - instances[defaultInstance] = new ExperimentClient(apiKey, config); + // Store instances by appending the instance name and api key. Allows for + // initializing multiple default instances for different api keys. + const instanceName = config?.instanceName || Defaults.instanceName; + const instanceKey = `${instanceName}.${apiKey}`; + if (!instances[instanceKey]) { + instances[instanceKey] = new ExperimentClient(apiKey, config); } - return instances[defaultInstance]; + return instances[instanceKey]; }; -const initializeWithAmplitude = ( +/** + * Initialize a singleton {@link ExperimentClient} which automatically + * integrates with the installed and initialized instance of the amplitude + * analytics SDK. + * + * Amplitude analytics + * @param apiKey + * @param config + */ +const initializeWithAmplitudeAnalytics = ( apiKey: string, config?: ExperimentConfig, ): ExperimentClient => { - const core = AmplitudeCore.getInstance(defaultInstance); - if (!instances[defaultInstance]) { + // Store instances by appending the instance name and api key. Allows for + // initializing multiple default instances for different api keys. + const instanceName = config?.instanceName || Defaults.instanceName; + const instanceKey = `${instanceName}.${apiKey}`; + const core = AmplitudeCore.getInstance(instanceName); + if (!instances[instanceKey]) { config = config || {}; if (!config.userProvider) { config.userProvider = new CoreUserProvider(core.identityStore); @@ -40,12 +55,12 @@ const initializeWithAmplitude = ( core.analyticsConnector, ); } - instances[defaultInstance] = new ExperimentClient(apiKey, config); + instances[instanceKey] = new ExperimentClient(apiKey, config); core.identityStore.addIdentityListener(() => { - instances[defaultInstance].fetch(); + instances[instanceKey].fetch(); }); } - return instances[defaultInstance]; + return instances[instanceKey]; }; /** @@ -54,5 +69,5 @@ const initializeWithAmplitude = ( */ export const Experiment = { initialize, - initializeWithAmplitude, + initializeWithAmplitudeAnalytics, }; diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index 2d189d61..5bd28677 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -47,7 +47,7 @@ export class CoreUserProvider implements ExperimentUserProvider { user_id: identity.userId, device_id: identity.deviceId, user_properties: userProperties, - // TODO: Other contextual info + // TODO: Other contextual info, should be contained in core }; } } diff --git a/packages/browser/test/factory.test.ts b/packages/browser/test/factory.test.ts new file mode 100644 index 00000000..7c12e24b --- /dev/null +++ b/packages/browser/test/factory.test.ts @@ -0,0 +1,32 @@ +import { Experiment } from 'src/factory'; + +const API_KEY = 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3'; +const OTHER_KEY = 'some-other-key'; + +beforeEach(() => { + localStorage.clear(); +}); + +test('Experiment.initialize, default instance name and api key, same object', async () => { + const client1 = Experiment.initialize(API_KEY); + const client2 = Experiment.initialize(API_KEY, { + instanceName: '$default_instance', + }); + expect(client2).toBe(client1); +}); + +test('Experiment.initialize, custom instance name, same object', async () => { + const client1 = Experiment.initialize(API_KEY, { + instanceName: 'brian', + }); + const client2 = Experiment.initialize(API_KEY, { + instanceName: 'brian', + }); + expect(client2).toBe(client1); +}); + +test('Experiment.initialize, same instance name, different api key, different object', async () => { + const client1 = Experiment.initialize(API_KEY); + const client2 = Experiment.initialize(OTHER_KEY); + expect(client2).not.toBe(client1); +}); diff --git a/yarn.lock b/yarn.lock index 034006ad..8fe03463 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,7 +3,7 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0-alpha.5" + version "1.4.0-alpha.8" dependencies: "@amplitude/amplitude-core" "0.0.3" base64-js "1.5.1" From c522ed2bc0ea5eea2834409eec46de05ea2f97c0 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 17 Dec 2021 13:04:45 -0800 Subject: [PATCH 46/68] add app context provider to core --- packages/core/src/amplitudeCore.ts | 3 ++ .../core/src/applicationContextProvider.ts | 49 +++++++++++++++++++ packages/core/src/index.ts | 4 ++ packages/core/src/util/global.ts | 2 - 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/applicationContextProvider.ts diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index f29a14b2..5e37a8a7 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -1,4 +1,5 @@ import { AnalyticsConnectorImpl } from './analyticsConnector'; +import { ApplicationContextProviderImpl } from './applicationContextProvider'; import { IdentityStoreImpl } from './identityStore'; import { safeGlobal } from './util/global'; @@ -7,6 +8,8 @@ safeGlobal['amplitudeCoreInstances'] = {}; export class AmplitudeCore { public readonly identityStore = new IdentityStoreImpl(); public readonly analyticsConnector = new AnalyticsConnectorImpl(); + public readonly applicationContextProvider = + new ApplicationContextProviderImpl(); static getInstance(instanceName: string): AmplitudeCore { if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { diff --git a/packages/core/src/applicationContextProvider.ts b/packages/core/src/applicationContextProvider.ts new file mode 100644 index 00000000..1b94dda5 --- /dev/null +++ b/packages/core/src/applicationContextProvider.ts @@ -0,0 +1,49 @@ +import { UAParser } from '@amplitude/ua-parser-js'; + +export type ApplicationContext = { + versionName?: string; + language?: string; + platform?: string; + os?: string; + deviceModel?: string; +}; + +export interface ApplicationContextProvider { + versionName: string; + getApplicationContext(): ApplicationContext; +} + +export class ApplicationContextProviderImpl + implements ApplicationContextProvider +{ + private readonly ua = new UAParser(navigator.userAgent).getResult(); + public versionName: string; + getApplicationContext(): ApplicationContext { + return { + versionName: this.versionName, + language: getLanguage(), + platform: 'Web', + os: getOs(this.ua), + deviceModel: getDeviceModel(this.ua), + }; + } +} + +const getOs = (ua: UAParser): string => { + return [ua.browser?.name, ua.browser?.major] + .filter((e) => e !== null && e !== undefined) + .join(' '); +}; + +const getDeviceModel = (ua: UAParser): string => { + return ua.os?.name; +}; + +const getLanguage = (): string => { + return ( + (typeof navigator !== 'undefined' && + ((navigator.languages && navigator.languages[0]) || + navigator.language)) || + '' + ); +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e3f7b1ab..f2487bd2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,10 @@ export { AnalyticsEvent, AnalyticsEventReceiver, } from './analyticsConnector'; +export { + ApplicationContext, + ApplicationContextProvider, +} from './applicationContextProvider'; export { Identity, IdentityStore, diff --git a/packages/core/src/util/global.ts b/packages/core/src/util/global.ts index e35fa2e3..e399c678 100644 --- a/packages/core/src/util/global.ts +++ b/packages/core/src/util/global.ts @@ -1,4 +1,2 @@ -type GlobalType = typeof globalThis | (NodeJS.Global & typeof globalThis); - export const safeGlobal = typeof globalThis !== 'undefined' ? globalThis : global || self; From 512cf735653ccdc69cbb7fcb46c5b928670d31dc Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 20 Dec 2021 11:31:41 -0800 Subject: [PATCH 47/68] default user provider --- packages/browser/src/factory.ts | 20 +++++++++-------- packages/browser/src/integration/default.ts | 22 +++++++++++++++++++ .../core/src/applicationContextProvider.ts | 15 +++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 packages/browser/src/integration/default.ts diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index 47c243ac..e16c6361 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -3,6 +3,7 @@ import { AmplitudeCore } from '@amplitude/amplitude-core'; import { Defaults, ExperimentConfig } from './config'; import { ExperimentClient } from './experimentClient'; import { CoreAnalyticsProvider, CoreUserProvider } from './integration/core'; +import { DefaultUserProvider } from './integration/default'; const instances = {}; @@ -21,7 +22,12 @@ const initialize = ( // initializing multiple default instances for different api keys. const instanceName = config?.instanceName || Defaults.instanceName; const instanceKey = `${instanceName}.${apiKey}`; + const core = AmplitudeCore.getInstance(instanceName); if (!instances[instanceKey]) { + config = { + userProvider: new DefaultUserProvider(core.applicationContextProvider), + ...config, + }; instances[instanceKey] = new ExperimentClient(apiKey, config); } return instances[instanceKey]; @@ -46,15 +52,11 @@ const initializeWithAmplitudeAnalytics = ( const instanceKey = `${instanceName}.${apiKey}`; const core = AmplitudeCore.getInstance(instanceName); if (!instances[instanceKey]) { - config = config || {}; - if (!config.userProvider) { - config.userProvider = new CoreUserProvider(core.identityStore); - } - if (!config.analyticsProvider) { - config.analyticsProvider = new CoreAnalyticsProvider( - core.analyticsConnector, - ); - } + config = { + userProvider: new CoreUserProvider(core.identityStore), + analyticsProvider: new CoreAnalyticsProvider(core.analyticsConnector), + ...config, + }; instances[instanceKey] = new ExperimentClient(apiKey, config); core.identityStore.addIdentityListener(() => { instances[instanceKey].fetch(); diff --git a/packages/browser/src/integration/default.ts b/packages/browser/src/integration/default.ts new file mode 100644 index 00000000..eb4b095a --- /dev/null +++ b/packages/browser/src/integration/default.ts @@ -0,0 +1,22 @@ +import { ApplicationContextProvider } from '@amplitude/amplitude-core'; + +import { ExperimentUserProvider } from '../types/provider'; +import { ExperimentUser } from '../types/user'; + +export class DefaultUserProvider implements ExperimentUserProvider { + private readonly contextProvider: ApplicationContextProvider; + constructor(applicationContextProvider: ApplicationContextProvider) { + this.contextProvider = applicationContextProvider; + } + + getUser(): ExperimentUser { + const context = this.contextProvider.getApplicationContext(); + return { + version: context.versionName, + language: context.language, + platform: context.platform, + os: context.os, + device_model: context.deviceModel, + }; + } +} diff --git a/packages/core/src/applicationContextProvider.ts b/packages/core/src/applicationContextProvider.ts index 1b94dda5..0fc87c3a 100644 --- a/packages/core/src/applicationContextProvider.ts +++ b/packages/core/src/applicationContextProvider.ts @@ -13,9 +13,20 @@ export interface ApplicationContextProvider { getApplicationContext(): ApplicationContext; } +export const getApplicationContext = ( + versionName: string = null, +): ApplicationContext => { + return { + versionName: versionName, + language: getLanguage(), + platform: 'Web', + os: getOs(this.ua), + deviceModel: getDeviceModel(this.ua), + }; +}; + export class ApplicationContextProviderImpl - implements ApplicationContextProvider -{ + implements ApplicationContextProvider { private readonly ua = new UAParser(navigator.userAgent).getResult(); public versionName: string; getApplicationContext(): ApplicationContext { From cf214a1c697f400a88a9048f1b1289d89322be68 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 20 Dec 2021 11:47:31 -0800 Subject: [PATCH 48/68] 1.0.0-alpha.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 70544884..ecc75f3c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/amplitude-core", - "version": "0.0.3", + "version": "1.0.0-alpha.0", "description": "Core package for Amplitide SDKs", "main": "dist/core.umd.js", "types": "dist/types/src/index.d.ts", From 8428badb0da9d69ba4657efd51e3b440f37366b0 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 20 Dec 2021 12:14:45 -0800 Subject: [PATCH 49/68] remove unused function causing problems --- packages/browser/package.json | 2 +- packages/core/src/applicationContextProvider.ts | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index cd30b773..f6f54611 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "0.0.3" + "@amplitude/amplitude-core": "1.0.0-alpha.0" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/packages/core/src/applicationContextProvider.ts b/packages/core/src/applicationContextProvider.ts index 0fc87c3a..ed1991a9 100644 --- a/packages/core/src/applicationContextProvider.ts +++ b/packages/core/src/applicationContextProvider.ts @@ -13,18 +13,6 @@ export interface ApplicationContextProvider { getApplicationContext(): ApplicationContext; } -export const getApplicationContext = ( - versionName: string = null, -): ApplicationContext => { - return { - versionName: versionName, - language: getLanguage(), - platform: 'Web', - os: getOs(this.ua), - deviceModel: getDeviceModel(this.ua), - }; -}; - export class ApplicationContextProviderImpl implements ApplicationContextProvider { private readonly ua = new UAParser(navigator.userAgent).getResult(); From 0b1a2066e65f49af78aaf532b0dcec63582b7b23 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 20 Dec 2021 12:34:42 -0800 Subject: [PATCH 50/68] 1.0.0-alpha.10 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index f6f54611..fc340392 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.9", + "version": "1.4.0-alpha.10", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From c9ffcc706ebab4a474dc12e03bd2d6926631a913 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 10 Jan 2022 12:23:11 -0800 Subject: [PATCH 51/68] remove non-idempotent id operations --- packages/core/src/identityStore.ts | 38 -------- packages/core/test/identityStore.test.ts | 117 ----------------------- 2 files changed, 155 deletions(-) diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts index f7efd15a..c8038641 100644 --- a/packages/core/src/identityStore.ts +++ b/packages/core/src/identityStore.ts @@ -1,9 +1,5 @@ const ID_OP_SET = '$set'; const ID_OP_UNSET = '$unset'; -const ID_OP_SET_ONCE = '$setOnce'; -const ID_OP_ADD = '$add'; -const ID_OP_APPEND = '$append'; -const ID_OP_PREPEND = '$prepend'; const ID_OP_CLEAR_ALL = '$clearAll'; export type Identity = { @@ -78,40 +74,6 @@ export class IdentityStoreImpl implements IdentityStore { delete actingProperties[key]; } break; - case ID_OP_SET_ONCE: - for (const [key, value] of Object.entries(properties)) { - if (!actingProperties[key]) { - actingProperties[key] = value; - } - } - break; - case ID_OP_ADD: - for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key] ?? 0; - if ( - typeof actingValue === 'number' && - typeof value === 'number' - ) { - actingProperties[key] = actingValue + value; - } - } - break; - case ID_OP_APPEND: - for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key] ?? []; - if (Array.isArray(actingValue) && Array.isArray(value)) { - actingProperties[key] = actingValue.concat(value); - } - } - break; - case ID_OP_PREPEND: - for (const [key, value] of Object.entries(properties)) { - const actingValue = actingProperties[key] ?? []; - if (Array.isArray(actingValue) && Array.isArray(value)) { - actingProperties[key] = value.concat(actingValue); - } - } - break; case ID_OP_CLEAR_ALL: actingProperties = {}; break; diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts index 24ebd371..ba29d4f2 100644 --- a/packages/core/test/identityStore.test.ts +++ b/packages/core/test/identityStore.test.ts @@ -134,120 +134,3 @@ test('updateUserProperties, unset', async () => { const identity = identityStore.getIdentity(); expect(identity).toEqual({ userProperties: {} }); }); - -test('updateUserProperties, setOnce', async () => { - const identityStore = new IdentityStoreImpl(); - let identify = new amplitude.Identify().setOnce('key', 'value1'); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - let identity = identityStore.getIdentity(); - expect(identity).toEqual({ userProperties: { key: 'value1' } }); - identify = new amplitude.Identify().setOnce('key', 'value2'); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - identity = identityStore.getIdentity(); - expect(identity).toEqual({ userProperties: { key: 'value1' } }); -}); - -test('updateUserProperties, add to exiting number', async () => { - const identityStore = new IdentityStoreImpl(); - const identify = new amplitude.Identify().set('int', 1).set('double', 1.1); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const add = new amplitude.Identify().add('int', 1.1).add('double', 1); - identityStore - .editIdentity() - .updateUserProperties(add['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - int: 2.1, - double: 2.1, - }, - }); -}); - -test('updateUserProperties, add to unset property', async () => { - const identityStore = new IdentityStoreImpl(); - const add = new amplitude.Identify().add('int', 1).add('double', 1.1); - identityStore - .editIdentity() - .updateUserProperties(add['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - int: 1, - double: 1.1, - }, - }); -}); - -test('updateUserProperties, append existing array', async () => { - const identityStore = new IdentityStoreImpl(); - identityStore.setIdentity({ userProperties: { key: [-3, -2, -1, 0] } }); - const identify = new amplitude.Identify().append('key', [1, 2, 3]); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - key: [-3, -2, -1, 0, 1, 2, 3], - }, - }); -}); - -test('updateUserProperties, append to unset property', async () => { - const identityStore = new IdentityStoreImpl(); - const identify = new amplitude.Identify().append('key', [1, 2, 3]); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - key: [1, 2, 3], - }, - }); -}); - -test('updateUserProperties, prepend to existing array', async () => { - const identityStore = new IdentityStoreImpl(); - identityStore.setIdentity({ userProperties: { key: [0, 1, 2, 3] } }); - const identify = new amplitude.Identify().prepend('key', [-3, -2, -1]); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - key: [-3, -2, -1, 0, 1, 2, 3], - }, - }); -}); - -test('updateUserProperties, prepend to unset array', async () => { - const identityStore = new IdentityStoreImpl(); - const identify = new amplitude.Identify().prepend('key', [1, 2, 3]); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - key: [1, 2, 3], - }, - }); -}); From ae3b908dea04477878660171e4278bb4c7f18c41 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Tue, 11 Jan 2022 18:29:09 -0800 Subject: [PATCH 52/68] use session analytics provider by default; config for auto cset --- packages/browser/src/config.ts | 9 ++++ packages/browser/src/experimentClient.ts | 46 ++++++++++------- packages/browser/src/integration/core.ts | 21 -------- .../src/util/sessionAnalyticsProvider.ts | 49 +++++++++++++++++++ 4 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 packages/browser/src/util/sessionAnalyticsProvider.ts diff --git a/packages/browser/src/config.ts b/packages/browser/src/config.ts index 55276f8a..dc2c3d41 100644 --- a/packages/browser/src/config.ts +++ b/packages/browser/src/config.ts @@ -57,6 +57,13 @@ export interface ExperimentConfig { */ retryFetchOnFailure?: boolean; + /** + * If true, automatically tracks exposure events though the + * `ExperimentAnalyticsProvider`. If no analytics provider is set, this + * option does nothing. + */ + automaticClientSideExposureTracking?: boolean; + /** * Sets a user provider that will inject identity information into the user * for {@link fetch()} requests. The user provider will only set user fields @@ -86,6 +93,7 @@ export interface ExperimentConfig { | **serverUrl** | `"https://api.lab.amplitude.com"` | | **assignmentTimeoutMillis** | `10000` | | **retryFailedAssignment** | `true` | + | **automaticClientSideExposureTracking** | `true` | | **userProvider** | `null` | | **analyticsProvider** | `null` | @@ -101,6 +109,7 @@ export const Defaults: ExperimentConfig = { serverUrl: 'https://api.lab.amplitude.com', fetchTimeoutMillis: 10000, retryFetchOnFailure: true, + automaticClientSideExposureTracking: true, userProvider: null, analyticsProvider: null, }; diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index 907b62b7..8a2109d4 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -21,6 +21,7 @@ import { isNullOrUndefined } from './util'; import { Backoff } from './util/backoff'; import { urlSafeBase64Encode } from './util/base64'; import { randomString } from './util/randomstring'; +import { SessionAnalyticsProvider } from './util/sessionAnalyticsProvider'; // Configs which have been removed from the public API. // May be added back in the future. @@ -50,6 +51,8 @@ export class ExperimentClient implements Client { */ private userProvider: ExperimentUserProvider = null; + private analyticsProvider: SessionAnalyticsProvider; + /** * Creates a new ExperimentClient instance. * @@ -65,6 +68,11 @@ export class ExperimentClient implements Client { if (this.config.userProvider) { this.userProvider = this.config.userProvider; } + if (this.config.analyticsProvider) { + this.analyticsProvider = new SessionAnalyticsProvider( + this.config.analyticsProvider, + ); + } this.httpClient = FetchHttpClient; this.storage = new LocalStorage(this.config.instanceName, apiKey); this.storage.load(); @@ -128,26 +136,26 @@ export class ExperimentClient implements Client { return { value: undefined }; } const { source, variant } = this.variantAndSource(key, fallback); - - if (isFallback(source) || !variant?.value) { - // fallbacks indicate not being allocated into an experiment, so - // we can unset the property - this.addContext(this.getUser()).then((user) => { - this.config.analyticsProvider?.unsetUserProperty?.( - exposureEvent(user, key, variant, source), - ); - }); - } else if (variant?.value) { - // fallbacks indicate not being allocated into an experiment, so - // we can unset the property - this.addContext(this.getUser()).then((user) => { - // only track when there's a value for a non fallback variant - const event = exposureEvent(user, key, variant, source); - this.config.analyticsProvider?.setUserProperty?.(event); - this.config.analyticsProvider?.track(event); - }); + if (this.config.automaticClientSideExposureTracking) { + if (isFallback(source) || !variant?.value) { + // fallbacks indicate not being allocated into an experiment, so + // we can unset the property + this.addContext(this.getUser()).then((user) => { + this.analyticsProvider?.unsetUserProperty?.( + exposureEvent(user, key, variant, source), + ); + }); + } else if (variant?.value) { + // fallbacks indicate not being allocated into an experiment, so + // we can unset the property + this.addContext(this.getUser()).then((user) => { + // only track when there's a value for a non fallback variant + const event = exposureEvent(user, key, variant, source); + this.analyticsProvider?.setUserProperty?.(event); + this.analyticsProvider?.track(event); + }); + } } - this.debug(`[Experiment] variant for ${key} is ${variant.value}`); return variant; } diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index 5bd28677..bded3f18 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -55,23 +55,11 @@ export class CoreUserProvider implements ExperimentUserProvider { export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { private readonly analyticsConnector: AnalyticsConnector; - // In memory record of flagKey and variant value to in order to only set - // user properties and track an exposure event once per session unless the - // variant value changes - private readonly setProperties: Record = {}; - private readonly unsetProperties: Record = {}; - constructor(analyticsConnector: AnalyticsConnector) { this.analyticsConnector = analyticsConnector; } track(event: ExperimentAnalyticsEvent): void { - if (this.setProperties[event.key] == event.variant.value) { - return; - } else { - this.setProperties[event.key] = event.variant.value; - delete this.unsetProperties[event.key]; - } const analyticsEvent: AnalyticsEvent = { eventType: event.name, eventProperties: event.properties, @@ -81,9 +69,6 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { } setUserProperty?(event: ExperimentAnalyticsEvent): void { - if (this.setProperties[event.key] == event.variant.value) { - return; - } const analyticsEvent: AnalyticsEvent = { eventType: '$identify', userProperties: { @@ -94,12 +79,6 @@ export class CoreAnalyticsProvider implements ExperimentAnalyticsProvider { } unsetUserProperty?(event: ExperimentAnalyticsEvent): void { - if (this.unsetProperties[event.key]) { - return; - } else { - this.unsetProperties[event.key] = 'unset'; - delete this.setProperties[event.key]; - } const analyticsEvent: AnalyticsEvent = { eventType: '$identify', userProperties: { diff --git a/packages/browser/src/util/sessionAnalyticsProvider.ts b/packages/browser/src/util/sessionAnalyticsProvider.ts new file mode 100644 index 00000000..0bb4c7a6 --- /dev/null +++ b/packages/browser/src/util/sessionAnalyticsProvider.ts @@ -0,0 +1,49 @@ +import { ExperimentAnalyticsEvent } from '../types/analytics'; +import { ExperimentAnalyticsProvider } from '../types/provider'; + +/** + * A wrapper for an analytics provider which only sends one exposure event per + * flag, per variant, per session. In other words, wrapping an analytics + * provider in this class will prevent the same exposure event to be sent twice + * in one session. + */ +export class SessionAnalyticsProvider implements ExperimentAnalyticsProvider { + private readonly analyticsProvider: ExperimentAnalyticsProvider; + + // In memory record of flagKey and variant value to in order to only set + // user properties and track an exposure event once per session unless the + // variant value changes + private readonly setProperties: Record = {}; + private readonly unsetProperties: Record = {}; + + constructor(analyticsProvider: ExperimentAnalyticsProvider) { + this.analyticsProvider = analyticsProvider; + } + + track(event: ExperimentAnalyticsEvent): void { + if (this.setProperties[event.key] == event.variant.value) { + return; + } else { + this.setProperties[event.key] = event.variant.value; + delete this.unsetProperties[event.key]; + } + this.analyticsProvider.track(event); + } + + setUserProperty?(event: ExperimentAnalyticsEvent): void { + if (this.setProperties[event.key] == event.variant.value) { + return; + } + this.analyticsProvider.setUserProperty(event); + } + + unsetUserProperty?(event: ExperimentAnalyticsEvent): void { + if (this.unsetProperties[event.key]) { + return; + } else { + this.unsetProperties[event.key] = 'unset'; + delete this.setProperties[event.key]; + } + this.analyticsProvider.unsetUserProperty(event); + } +} From c4790ee000e89538b9f7700936b6a9c123df7d16 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 13 Jan 2022 15:25:29 -0800 Subject: [PATCH 53/68] remove core changes --- packages/browser/package.json | 2 +- packages/core/CHANGELOG.md | 0 packages/core/jest.config.js | 21 --- packages/core/package.json | 35 ---- packages/core/rollup.config.js | 43 ----- packages/core/src/amplitudeCore.ts | 20 --- packages/core/src/analyticsConnector.ts | 37 ---- .../core/src/applicationContextProvider.ts | 48 ------ packages/core/src/identityStore.ts | 161 ------------------ packages/core/src/index.ts | 16 -- packages/core/src/util/global.ts | 2 - packages/core/test/amplitudeCore.test.ts | 10 -- packages/core/test/analyticsConnector.test.ts | 31 ---- packages/core/test/identityStore.test.ts | 136 --------------- packages/core/tsconfig.json | 24 --- packages/core/tsconfig.test.json | 13 -- 16 files changed, 1 insertion(+), 598 deletions(-) delete mode 100644 packages/core/CHANGELOG.md delete mode 100644 packages/core/jest.config.js delete mode 100644 packages/core/package.json delete mode 100644 packages/core/rollup.config.js delete mode 100644 packages/core/src/amplitudeCore.ts delete mode 100644 packages/core/src/analyticsConnector.ts delete mode 100644 packages/core/src/applicationContextProvider.ts delete mode 100644 packages/core/src/identityStore.ts delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/util/global.ts delete mode 100644 packages/core/test/amplitudeCore.test.ts delete mode 100644 packages/core/test/analyticsConnector.test.ts delete mode 100644 packages/core/test/identityStore.test.ts delete mode 100644 packages/core/tsconfig.json delete mode 100644 packages/core/tsconfig.test.json diff --git a/packages/browser/package.json b/packages/browser/package.json index fc340392..9cff4b53 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0-alpha.10", + "version": "1.4.0", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js deleted file mode 100644 index d1e5f0e6..00000000 --- a/packages/core/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const { pathsToModuleNameMapper } = require('ts-jest/utils'); - -const package = require('./package'); -const { compilerOptions } = require('./tsconfig.test.json'); - -module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', - displayName: package.name, - name: package.name, - rootDir: '.', - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { - prefix: '/', - }), - globals: { - 'ts-jest': { - tsconfig: '/tsconfig.test.json', - }, - }, -}; diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index ecc75f3c..00000000 --- a/packages/core/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@amplitude/amplitude-core", - "version": "1.0.0-alpha.0", - "description": "Core package for Amplitide SDKs", - "main": "dist/core.umd.js", - "types": "dist/types/src/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "rm -rf dist && rollup -c", - "docs": "typedoc", - "lint": "eslint . --ignore-path ../../.eslintignore && prettier -c . --ignore-path ../../.prettierignore", - "test": "jest", - "version": "yarn docs && git add ../../docs", - "prepublish": "yarn build" - }, - "repository": { - "type": "git", - "url": "https://github.com/amplitude/experiment-js-client.git", - "directory": "packages/core" - }, - "author": "Amplitude", - "license": "MIT", - "private": false, - "bugs": { - "url": "https://github.com/amplitude/experiment-js-client/issues" - }, - "homepage": "https://github.com/amplitude/experiment-js-client#readme", - "devDependencies": { - "@types/amplitude-js": "^8.0.2", - "amplitude-js": "^8.12.0" - }, - "gitHead": "0a910f04a64dafcf37b68be45ed7dca58fdd6acf" -} diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js deleted file mode 100644 index 63a1f5c9..00000000 --- a/packages/core/rollup.config.js +++ /dev/null @@ -1,43 +0,0 @@ -import babel from '@rollup/plugin-babel'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import resolve from '@rollup/plugin-node-resolve'; -import replace from '@rollup/plugin-replace'; -import typescript from '@rollup/plugin-typescript'; - -import tsConfig from './tsconfig.json'; - -const browserConfig = { - input: 'src/index.ts', - output: { - dir: 'dist', - entryFileNames: 'core.umd.js', - exports: 'named', - format: 'umd', - name: 'Experiment', - }, - treeshake: { - moduleSideEffects: 'no-external', - }, - external: [], - plugins: [ - replace({ BUILD_BROWSER: true }), - resolve(), - json(), - commonjs(), - typescript({ - declaration: true, - declarationDir: 'dist/types', - include: tsConfig.include, - rootDir: '.', - }), - babel({ - babelHelpers: 'bundled', - exclude: ['node_modules/**'], - }), - ], -}; - -const configs = [browserConfig]; - -export default configs; diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts deleted file mode 100644 index 5e37a8a7..00000000 --- a/packages/core/src/amplitudeCore.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AnalyticsConnectorImpl } from './analyticsConnector'; -import { ApplicationContextProviderImpl } from './applicationContextProvider'; -import { IdentityStoreImpl } from './identityStore'; -import { safeGlobal } from './util/global'; - -safeGlobal['amplitudeCoreInstances'] = {}; - -export class AmplitudeCore { - public readonly identityStore = new IdentityStoreImpl(); - public readonly analyticsConnector = new AnalyticsConnectorImpl(); - public readonly applicationContextProvider = - new ApplicationContextProviderImpl(); - - static getInstance(instanceName: string): AmplitudeCore { - if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { - safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); - } - return safeGlobal['amplitudeCoreInstances'][instanceName]; - } -} diff --git a/packages/core/src/analyticsConnector.ts b/packages/core/src/analyticsConnector.ts deleted file mode 100644 index 04f92536..00000000 --- a/packages/core/src/analyticsConnector.ts +++ /dev/null @@ -1,37 +0,0 @@ -export type AnalyticsEvent = { - eventType: string; - eventProperties?: Record; - userProperties?: Record; -}; - -export type AnalyticsEventReceiver = (event: AnalyticsEvent) => void; - -export interface AnalyticsConnector { - logEvent(event: AnalyticsEvent): void; - setEventReceiver(listener: AnalyticsEventReceiver): void; -} - -export class AnalyticsConnectorImpl implements AnalyticsConnector { - private receiver: AnalyticsEventReceiver; - private queue: AnalyticsEvent[] = []; - - logEvent(event: AnalyticsEvent): void { - if (!this.receiver) { - if (this.queue.length < 512) { - this.queue.push(event); - } - } else { - this.receiver(event); - } - } - - setEventReceiver(receiver: AnalyticsEventReceiver): void { - this.receiver = receiver; - if (this.queue.length > 0) { - this.queue.forEach((event) => { - receiver(event); - }); - this.queue = []; - } - } -} diff --git a/packages/core/src/applicationContextProvider.ts b/packages/core/src/applicationContextProvider.ts deleted file mode 100644 index ed1991a9..00000000 --- a/packages/core/src/applicationContextProvider.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { UAParser } from '@amplitude/ua-parser-js'; - -export type ApplicationContext = { - versionName?: string; - language?: string; - platform?: string; - os?: string; - deviceModel?: string; -}; - -export interface ApplicationContextProvider { - versionName: string; - getApplicationContext(): ApplicationContext; -} - -export class ApplicationContextProviderImpl - implements ApplicationContextProvider { - private readonly ua = new UAParser(navigator.userAgent).getResult(); - public versionName: string; - getApplicationContext(): ApplicationContext { - return { - versionName: this.versionName, - language: getLanguage(), - platform: 'Web', - os: getOs(this.ua), - deviceModel: getDeviceModel(this.ua), - }; - } -} - -const getOs = (ua: UAParser): string => { - return [ua.browser?.name, ua.browser?.major] - .filter((e) => e !== null && e !== undefined) - .join(' '); -}; - -const getDeviceModel = (ua: UAParser): string => { - return ua.os?.name; -}; - -const getLanguage = (): string => { - return ( - (typeof navigator !== 'undefined' && - ((navigator.languages && navigator.languages[0]) || - navigator.language)) || - '' - ); -}; diff --git a/packages/core/src/identityStore.ts b/packages/core/src/identityStore.ts deleted file mode 100644 index c8038641..00000000 --- a/packages/core/src/identityStore.ts +++ /dev/null @@ -1,161 +0,0 @@ -const ID_OP_SET = '$set'; -const ID_OP_UNSET = '$unset'; -const ID_OP_CLEAR_ALL = '$clearAll'; - -export type Identity = { - userId?: string; - deviceId?: string; - userProperties?: Record; -}; - -export type IdentityListener = (identity: Identity) => void; - -export interface IdentityStore { - editIdentity(): IdentityEditor; - getIdentity(): Identity; - setIdentity(identity: Identity): void; - addIdentityListener(listener: IdentityListener): void; - removeIdentityListener(listener: IdentityListener): void; -} - -export interface IdentityEditor { - setUserId(userId: string): IdentityEditor; - setDeviceId(deviceId: string): IdentityEditor; - setUserProperties(userProperties: Record): IdentityEditor; - updateUserProperties( - actions: Record>, - ): IdentityEditor; - commit(): void; -} - -export class IdentityStoreImpl implements IdentityStore { - private identity: Identity = { userProperties: {} }; - private listeners = new Set(); - - editIdentity(): IdentityEditor { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self: IdentityStore = this; - const actingUserProperties = { ...this.identity.userProperties }; - const actingIdentity: Identity = { - ...this.identity, - userProperties: actingUserProperties, - }; - return { - setUserId: function (userId: string): IdentityEditor { - actingIdentity.userId = userId; - return this; - }, - - setDeviceId: function (deviceId: string): IdentityEditor { - actingIdentity.deviceId = deviceId; - return this; - }, - - setUserProperties: function ( - userProperties: Record, - ): IdentityEditor { - actingIdentity.userProperties = userProperties; - return this; - }, - - updateUserProperties: function ( - actions: Record>, - ): IdentityEditor { - let actingProperties = actingIdentity.userProperties || {}; - for (const [action, properties] of Object.entries(actions)) { - switch (action) { - case ID_OP_SET: - for (const [key, value] of Object.entries(properties)) { - actingProperties[key] = value; - } - break; - case ID_OP_UNSET: - for (const key of Object.keys(properties)) { - delete actingProperties[key]; - } - break; - case ID_OP_CLEAR_ALL: - actingProperties = {}; - break; - } - } - actingIdentity.userProperties = actingProperties; - return this; - }, - - commit: function (): void { - self.setIdentity(actingIdentity); - return this; - }, - }; - } - - getIdentity(): Identity { - return { ...this.identity }; - } - - setIdentity(identity: Identity): void { - const originalIdentity = { ...this.identity }; - this.identity = { ...identity }; - if (!isEqual(originalIdentity, this.identity)) { - this.listeners.forEach((listener) => { - listener(identity); - }); - } - } - - addIdentityListener(listener: IdentityListener): void { - this.listeners.add(listener); - } - - removeIdentityListener(listener: IdentityListener): void { - this.listeners.delete(listener); - } -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isEqual = (obj1: any, obj2: any): boolean => { - const primitive = ['string', 'number', 'boolean', 'undefined']; - const typeA = typeof obj1; - const typeB = typeof obj2; - if (typeA !== typeB) { - return false; - } - if (primitive.includes(typeA)) { - return obj1 === obj2; - } - //if got here - objects - if (obj1.length !== obj2.length) { - return false; - } - //check if arrays - const isArrayA = Array.isArray(obj1); - const isArrayB = Array.isArray(obj2); - if (isArrayA !== isArrayB) { - return false; - } - if (isArrayA && isArrayB) { - //arrays - for (let i = 0; i < obj1.length; i++) { - if (!isEqual(obj1[i], obj2[i])) { - return false; - } - } - } else { - //objects - const sorted1 = Object.keys(obj1).sort(); - const sorted2 = Object.keys(obj2).sort(); - if (!isEqual(sorted1, sorted2)) { - return false; - } - //compare object values - let result = true; - Object.keys(obj1).forEach((key) => { - if (!isEqual(obj1[key], obj2[key])) { - result = false; - } - }); - return result; - } - return true; -}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index f2487bd2..00000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { AmplitudeCore } from './amplitudeCore'; -export { - AnalyticsConnector, - AnalyticsEvent, - AnalyticsEventReceiver, -} from './analyticsConnector'; -export { - ApplicationContext, - ApplicationContextProvider, -} from './applicationContextProvider'; -export { - Identity, - IdentityStore, - IdentityListener, - IdentityEditor, -} from './identityStore'; diff --git a/packages/core/src/util/global.ts b/packages/core/src/util/global.ts deleted file mode 100644 index e399c678..00000000 --- a/packages/core/src/util/global.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const safeGlobal = - typeof globalThis !== 'undefined' ? globalThis : global || self; diff --git a/packages/core/test/amplitudeCore.test.ts b/packages/core/test/amplitudeCore.test.ts deleted file mode 100644 index 9736f375..00000000 --- a/packages/core/test/amplitudeCore.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AmplitudeCore } from '../src/amplitudeCore'; - -test('getAmplitudeCore returns the same instance', async () => { - const core = AmplitudeCore.getInstance('$default_instance'); - core.identityStore.setIdentity({ userId: 'userId' }); - - const core2 = AmplitudeCore.getInstance('$default_instance'); - const identity = core2.identityStore.getIdentity(); - expect(identity).toEqual({ userId: 'userId' }); -}); diff --git a/packages/core/test/analyticsConnector.test.ts b/packages/core/test/analyticsConnector.test.ts deleted file mode 100644 index 430d1a00..00000000 --- a/packages/core/test/analyticsConnector.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AnalyticsConnectorImpl } from '../src/analyticsConnector'; - -test('addEventListener, logEvent, listner called', async () => { - const analyticsConnector = new AnalyticsConnectorImpl(); - const expectedEvent = { eventType: 'test' }; - analyticsConnector.setEventReceiver((event) => { - expect(event).toEqual(expectedEvent); - }); -}); - -test('multiple logEvent, late addEventListener, listner called', async () => { - const expectedEvent0 = { eventType: 'test0' }; - const expectedEvent1 = { eventType: 'test1' }; - const expectedEvent2 = { eventType: 'test2' }; - const analyticsConnector = new AnalyticsConnectorImpl(); - analyticsConnector.logEvent(expectedEvent0); - analyticsConnector.logEvent(expectedEvent1); - analyticsConnector.logEvent(expectedEvent2); - let count = 0; - analyticsConnector.setEventReceiver((event) => { - if (count == 0) { - expect(event).toEqual(expectedEvent0); - } else if (count == 1) { - expect(event).toEqual(expectedEvent1); - } else if (count == 2) { - expect(event).toEqual(expectedEvent2); - } - count++; - }); - expect(count).toEqual(3); -}); diff --git a/packages/core/test/identityStore.test.ts b/packages/core/test/identityStore.test.ts deleted file mode 100644 index ba29d4f2..00000000 --- a/packages/core/test/identityStore.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* eslint-disable no-console */ -import amplitude from 'amplitude-js'; - -import { IdentityStoreImpl } from '../src/identityStore'; - -test('editIdentity, setUserId setDeviceId, success', async () => { - const identityStore = new IdentityStoreImpl(); - identityStore - .editIdentity() - .setUserId('user_id') - .setDeviceId('device_id') - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userId: 'user_id', - deviceId: 'device_id', - userProperties: {}, - }); -}); - -test('editIdentity, setUserId setDeviceId, identity listener called', async () => { - const identityStore = new IdentityStoreImpl(); - const expectedIdentity = { - userId: 'user_id', - deviceId: 'device_id', - userProperties: {}, - }; - let listenerCalled = false; - identityStore.addIdentityListener((identity) => { - expect(identity).toEqual(expectedIdentity); - listenerCalled = true; - }); - identityStore - .editIdentity() - .setUserId('user_id') - .setDeviceId('device_id') - .commit(); - expect(listenerCalled).toEqual(true); -}); - -test('editIdentity, updateUserProperties, identity listener called', async () => { - const identityStore = new IdentityStoreImpl(); - let listenerCalled = false; - identityStore.addIdentityListener(() => { - listenerCalled = true; - }); - - identityStore - .editIdentity() - .setUserId('user_id') - .setDeviceId('device_id') - .commit(); - expect(listenerCalled).toEqual(true); - - listenerCalled = false; - identityStore - .editIdentity() - .updateUserProperties({ $set: { test: 'test' } }) - .commit(); - expect(listenerCalled).toEqual(true); - - listenerCalled = false; - identityStore - .editIdentity() - .updateUserProperties({ $set: { test: 'test2' } }) - .commit(); - expect(listenerCalled).toEqual(true); -}); - -test('setIdentity, getIdentity, success', async () => { - const identityStore = new IdentityStoreImpl(); - const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; - identityStore.setIdentity(expectedIdentity); - const identity = identityStore.getIdentity(); - expect(identity).toEqual(expectedIdentity); -}); - -test('setIdentity, identity listener called', async () => { - const identityStore = new IdentityStoreImpl(); - const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; - let listenerCalled = false; - identityStore.addIdentityListener((identity) => { - expect(identity).toEqual(expectedIdentity); - listenerCalled = true; - }); - identityStore.setIdentity(expectedIdentity); - expect(listenerCalled).toEqual(true); -}); - -test('setIdentity with unchanged identity, identity listener not called', async () => { - const identityStore = new IdentityStoreImpl(); - const expectedIdentity = { userId: 'user_id', deviceId: 'device_id' }; - identityStore.setIdentity(expectedIdentity); - identityStore.addIdentityListener(() => { - fail('identity listener should not be called'); - }); - identityStore.setIdentity(expectedIdentity); -}); - -test('updateUserProperties, set', async () => { - const identityStore = new IdentityStoreImpl(); - const identify = new amplitude.Identify() - .set('string', 'string') - .set('int', 32) - .set('bool', true) - .set('double', 4.2) - .set('jsonArray', [0, 1.1, true, 'three']) - .set('jsonObject', { key: 'value' }); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ - userProperties: { - string: 'string', - int: 32, - bool: true, - double: 4.2, - jsonArray: [0, 1.1, true, 'three'], - jsonObject: { key: 'value' }, - }, - }); -}); - -test('updateUserProperties, unset', async () => { - const identityStore = new IdentityStoreImpl(); - identityStore.setIdentity({ userProperties: { key: 'value' } }); - const identify = new amplitude.Identify().unset('key'); - identityStore - .editIdentity() - .updateUserProperties(identify['userPropertiesOperations']) - .commit(); - const identity = identityStore.getIdentity(); - expect(identity).toEqual({ userProperties: {} }); -}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index c694f848..00000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src/**/*.ts", "package.json"], - "typedocOptions": { - "name": "Experiment JS Client Documentation", - "entryPoints": ["./src/index.ts"], - "categoryOrder": [ - "Core Usage", - "Configuration", - "Context Provider", - "Types" - ], - "categorizeByGroup": false, - "disableSources": true, - "excludePrivate": true, - "excludeProtected": true, - "excludeInternal": true, - "hideGenerator": true, - "includeVersion": true, - "out": "../../docs", - "readme": "none", - "theme": "minimal" - } -} diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json deleted file mode 100644 index a6ff84d1..00000000 --- a/packages/core/tsconfig.test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "rootDir": ".", - "baseUrl": ".", - "paths": { - "src/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "test/**/*.ts"], - "exclude": ["dist"] -} From c23a82b63b4804699d6416c54769bba9f1e39db6 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 13 Jan 2022 15:33:13 -0800 Subject: [PATCH 54/68] yarn lock --- yarn.lock | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8fe03463..d9e20918 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,15 @@ # yarn lockfile v1 +"@amplitude/amplitude-core@1.0.0-alpha.0": + version "1.0.0-alpha.0" + resolved "https://registry.yarnpkg.com/@amplitude/amplitude-core/-/amplitude-core-1.0.0-alpha.0.tgz#8a1297d3b936855e5d2583ced16a883f27c3751a" + integrity sha512-bi5fQMzVWZYN1fWna7UjzmmCPFFtqagF88HPRgX8duk9xnMgM4E4uqEHnc1UyE5aPoWR1eeHBRss29PINAySjA== + "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0-alpha.8" + version "1.4.0" dependencies: - "@amplitude/amplitude-core" "0.0.3" + "@amplitude/amplitude-core" "1.0.0-alpha.0" base64-js "1.5.1" unfetch "4.1.0" From beeb3fffb12863d7892f8c724302c206f3d8a96f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 13 Jan 2022 17:31:35 -0800 Subject: [PATCH 55/68] fix test --- packages/browser/test/client.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 59bef99f..7fe41bc8 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -1,4 +1,4 @@ -import { AnalyticsConnectorImpl } from '@amplitude/amplitude-core/src/analyticsConnector'; +import { AmplitudeCore } from '@amplitude/amplitude-core'; import { CoreAnalyticsProvider } from 'src/integration/core'; import { ExperimentClient } from '../src/experimentClient'; @@ -233,7 +233,7 @@ class TestAnalyticsProvider implements ExperimentAnalyticsProvider { } test('ExperimentClient.variant, with analytics provider, unset called only once per key', async () => { - const analyticsConnector = new AnalyticsConnectorImpl(); + const analyticsConnector = AmplitudeCore.getInstance('1').analyticsConnector; const analyticsProvider = new CoreAnalyticsProvider(analyticsConnector); const unsetSpy = jest.spyOn(analyticsProvider, 'unsetUserProperty'); let eventCount = 0; @@ -251,7 +251,7 @@ test('ExperimentClient.variant, with analytics provider, unset called only once // analytics provider call is asynchronous await delay(1000); - expect(unsetSpy).toBeCalledTimes(100); + expect(unsetSpy).toBeCalledTimes(1); expect(eventCount).toEqual(1); }); From 66995db41385c42aa82760360de85cafbc57e6ab Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 20 Jan 2022 18:20:19 -0800 Subject: [PATCH 56/68] add auto fetch on id change config --- packages/browser/src/config.ts | 17 +++++++++++++++++ packages/browser/src/factory.ts | 8 +++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/config.ts b/packages/browser/src/config.ts index dc2c3d41..8767b78e 100644 --- a/packages/browser/src/config.ts +++ b/packages/browser/src/config.ts @@ -64,6 +64,21 @@ export interface ExperimentConfig { */ automaticClientSideExposureTracking?: boolean; + /** + * This config only matters if you are using the amplitude analytics SDK + * integration initialized by calling + * `Experiment.initializeWithAmplitudeAnalytics()`. + * + * If true, the `ExperimentClient` will automatically fetch variants when the + * user's identity changes. The user's identity includes user_id, device_id + * and any user properties which are `set`, `unset` or `clearAll`ed via a call + * to `identify()`. + * + * Note: Non-idempotent identify operations `setOnce`, `add`, `append`, and + * `prepend` are not counted towards the user identity changing. + */ + automaticFetchOnAmplitudeIdentityChange?: boolean; + /** * Sets a user provider that will inject identity information into the user * for {@link fetch()} requests. The user provider will only set user fields @@ -94,6 +109,7 @@ export interface ExperimentConfig { | **assignmentTimeoutMillis** | `10000` | | **retryFailedAssignment** | `true` | | **automaticClientSideExposureTracking** | `true` | + | **automaticFetchOnAmplitudeIdentityChange** | `false` | | **userProvider** | `null` | | **analyticsProvider** | `null` | @@ -110,6 +126,7 @@ export const Defaults: ExperimentConfig = { fetchTimeoutMillis: 10000, retryFetchOnFailure: true, automaticClientSideExposureTracking: true, + automaticFetchOnAmplitudeIdentityChange: false, userProvider: null, analyticsProvider: null, }; diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index e16c6361..fbe40af8 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -58,9 +58,11 @@ const initializeWithAmplitudeAnalytics = ( ...config, }; instances[instanceKey] = new ExperimentClient(apiKey, config); - core.identityStore.addIdentityListener(() => { - instances[instanceKey].fetch(); - }); + if (config.automaticFetchOnAmplitudeIdentityChange) { + core.identityStore.addIdentityListener(() => { + instances[instanceKey].fetch(); + }); + } } return instances[instanceKey]; }; From 2ca3eca54ff0f5561aabf409509ef6589069eea0 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 21 Jan 2022 13:34:18 -0800 Subject: [PATCH 57/68] add timeout to waiting on identity --- packages/browser/src/experimentClient.ts | 36 +++++++++++++----------- packages/browser/src/integration/core.ts | 27 ++++++++++++------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index 8a2109d4..df0fedbc 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -140,20 +140,17 @@ export class ExperimentClient implements Client { if (isFallback(source) || !variant?.value) { // fallbacks indicate not being allocated into an experiment, so // we can unset the property - this.addContext(this.getUser()).then((user) => { - this.analyticsProvider?.unsetUserProperty?.( - exposureEvent(user, key, variant, source), - ); - }); + const user = this.addContext(this.getUser()); + const event = exposureEvent(user, key, variant, source); + this.analyticsProvider?.unsetUserProperty?.(event); } else if (variant?.value) { // fallbacks indicate not being allocated into an experiment, so // we can unset the property - this.addContext(this.getUser()).then((user) => { - // only track when there's a value for a non fallback variant - const event = exposureEvent(user, key, variant, source); - this.analyticsProvider?.setUserProperty?.(event); - this.analyticsProvider?.track(event); - }); + // only track when there's a value for a non fallback variant + const user = this.addContext(this.getUser()); + const event = exposureEvent(user, key, variant, source); + this.analyticsProvider?.setUserProperty?.(event); + this.analyticsProvider?.track(event); } } this.debug(`[Experiment] variant for ${key} is ${variant.value}`); @@ -342,7 +339,7 @@ export class ExperimentClient implements Client { user: ExperimentUser, timeoutMillis: number, ): Promise { - const userContext = await this.addContext(user); + const userContext = await this.addContextOrWait(user, 1000); const encodedContext = urlSafeBase64Encode(JSON.stringify(userContext)); let queryString = ''; if (this.config.debug) { @@ -409,10 +406,7 @@ export class ExperimentClient implements Client { } } - private async addContext(user: ExperimentUser): Promise { - if (this.userProvider instanceof CoreUserProvider) { - await this.userProvider.identityReady(); - } + private addContext(user: ExperimentUser): ExperimentUser { const providedUser = this.userProvider?.getUser(); const mergedUserProperties = { ...user?.user_properties, @@ -426,6 +420,16 @@ export class ExperimentClient implements Client { }; } + private async addContextOrWait( + user: ExperimentUser, + ms: number, + ): Promise { + if (this.userProvider instanceof CoreUserProvider) { + await this.userProvider.identityReady(ms); + } + return this.addContext(user); + } + private convertVariant(value: string | Variant): Variant { if (value === null || value === undefined) { return {}; diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index bded3f18..9c0ab6fc 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -10,6 +10,7 @@ import { ExperimentUserProvider, } from '../types/provider'; import { ExperimentUser } from '../types/user'; +import { safeGlobal } from '../util/global'; type UserProperties = Record< string, @@ -22,16 +23,26 @@ export class CoreUserProvider implements ExperimentUserProvider { this.identityStore = identityStore; } - async identityReady(): Promise { + async identityReady(ms: number): Promise { const identity = this.identityStore.getIdentity(); if (!identity.userId && !identity.deviceId) { - return new Promise((resolve) => { - const listener = () => { - resolve(); - this.identityStore.removeIdentityListener(listener); - }; - this.identityStore.addIdentityListener(listener); - }); + return Promise.race([ + new Promise((resolve) => { + const listener = () => { + resolve(); + this.identityStore.removeIdentityListener(listener); + }; + this.identityStore.addIdentityListener(listener); + }), + new Promise((resolve, reject) => { + safeGlobal.setTimeout( + reject, + ms, + 'Timed out waiting for Amplitude Analytics SDK to initialize. ' + + 'You must ensure that the analytics SDK is initialized prior to calling fetch().', + ); + }), + ]); } } From f80788fc6dc167fe3d190621393b449e14ee8b4d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 24 Jan 2022 12:31:09 -0800 Subject: [PATCH 58/68] fix comments; todos --- packages/browser/src/integration/amplitude.ts | 4 ++-- packages/browser/src/integration/core.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/integration/amplitude.ts b/packages/browser/src/integration/amplitude.ts index 9cc4fe98..c9cd3c35 100644 --- a/packages/browser/src/integration/amplitude.ts +++ b/packages/browser/src/integration/amplitude.ts @@ -37,7 +37,7 @@ type AmplitudeUAParser = { }; /** - * @deprecated Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * @deprecated Update your version of the amplitude analytics-js SDK to 8.17.0+ and for seamless * integration with the amplitude analytics SDK. */ export class AmplitudeUserProvider implements ExperimentUserProvider { @@ -73,7 +73,7 @@ export class AmplitudeUserProvider implements ExperimentUserProvider { } /** - * @deprecated Update your version of the amplitude analytics SDK to X.X.X+ and for seamless + * @deprecated Update your version of the amplitude analytics-js SDK to 8.17.0+ and for seamless * integration with the amplitude analytics SDK. */ export class AmplitudeAnalyticsProvider implements ExperimentAnalyticsProvider { diff --git a/packages/browser/src/integration/core.ts b/packages/browser/src/integration/core.ts index 9c0ab6fc..5a85d5d3 100644 --- a/packages/browser/src/integration/core.ts +++ b/packages/browser/src/integration/core.ts @@ -58,7 +58,6 @@ export class CoreUserProvider implements ExperimentUserProvider { user_id: identity.userId, device_id: identity.deviceId, user_properties: userProperties, - // TODO: Other contextual info, should be contained in core }; } } From 6ad9f256c0871b99b982434ebfc28bf9246b4a14 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Mon, 24 Jan 2022 12:50:01 -0800 Subject: [PATCH 59/68] update factory comment for integration --- packages/browser/src/factory.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/factory.ts b/packages/browser/src/factory.ts index fbe40af8..7fff0314 100644 --- a/packages/browser/src/factory.ts +++ b/packages/browser/src/factory.ts @@ -11,7 +11,7 @@ const instances = {}; * Initializes a singleton {@link ExperimentClient} identified by the configured * instance name. * - * @param apiKey The environment API Key + * @param apiKey The deployment API Key * @param config See {@link ExperimentConfig} for config options */ const initialize = ( @@ -38,9 +38,11 @@ const initialize = ( * integrates with the installed and initialized instance of the amplitude * analytics SDK. * - * Amplitude analytics - * @param apiKey - * @param config + * You must be using amplitude-js SDK version 8.17.0+ for this integration to + * work. + * + * @param apiKey The deployment API Key + * @param config See {@link ExperimentConfig} for config options */ const initializeWithAmplitudeAnalytics = ( apiKey: string, From 449f3dad77c350419c7d885e19227cc6d920122f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 09:40:59 -0800 Subject: [PATCH 60/68] use v1 of core --- packages/browser/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 9cff4b53..6bb383f2 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -30,7 +30,7 @@ "dependencies": { "base64-js": "1.5.1", "unfetch": "4.1.0", - "@amplitude/amplitude-core": "1.0.0-alpha.0" + "@amplitude/amplitude-core": "1.0.0" }, "devDependencies": { "@types/amplitude-js": "^8.0.2", diff --git a/yarn.lock b/yarn.lock index d9e20918..e154f2b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@amplitude/amplitude-core@1.0.0-alpha.0": - version "1.0.0-alpha.0" - resolved "https://registry.yarnpkg.com/@amplitude/amplitude-core/-/amplitude-core-1.0.0-alpha.0.tgz#8a1297d3b936855e5d2583ced16a883f27c3751a" - integrity sha512-bi5fQMzVWZYN1fWna7UjzmmCPFFtqagF88HPRgX8duk9xnMgM4E4uqEHnc1UyE5aPoWR1eeHBRss29PINAySjA== +"@amplitude/amplitude-core@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@amplitude/amplitude-core/-/amplitude-core-1.0.0.tgz#bb5defca1de54f63f6228fad462a5cbcd8e00485" + integrity sha512-VobJZObB7zo0PTxpPKRF5ow9b8FEfLHrDGYml8QF1Q1zTN0mN8P5daXUeGrCh56ZVXCM68vU7hb6F+8j3KLtJQ== "@amplitude/experiment-js-client@file:packages/browser": version "1.4.0" dependencies: - "@amplitude/amplitude-core" "1.0.0-alpha.0" + "@amplitude/amplitude-core" "1.0.0" base64-js "1.5.1" unfetch "4.1.0" From 8b14a616bf1503f9c315eee92e1f3e158324df90 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 09:56:23 -0800 Subject: [PATCH 61/68] remove delays --- packages/browser/test/client.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 7fe41bc8..fd28af4e 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -248,9 +248,6 @@ test('ExperimentClient.variant, with analytics provider, unset called only once client.variant('key-that-does-not-exist'); } - // analytics provider call is asynchronous - await delay(1000); - expect(unsetSpy).toBeCalledTimes(1); expect(eventCount).toEqual(1); }); @@ -265,15 +262,11 @@ test('ExperimentClient.variant, with analytics provider, exposure tracked, unset const spySet = jest.spyOn(analyticsProvider, 'setUserProperty'); const spyUnset = jest.spyOn(analyticsProvider, 'unsetUserProperty'); const client = new ExperimentClient(API_KEY, { - debug: true, analyticsProvider: analyticsProvider, }); await client.fetch(testUser); client.variant(serverKey); - // analytics provider call is asynchronous - await delay(1000); - expect(spySet).toBeCalledTimes(1); expect(spyTrack).toBeCalledTimes(1); @@ -321,9 +314,6 @@ test('ExperimentClient.variant, with analytics provider, exposure not tracked on client.variant(initialKey); client.variant(unknownKey); - // analytics provider call is asynchronous - await delay(1000); - expect(spyTrack).toHaveBeenCalledTimes(0); expect(spySet).toHaveBeenCalledTimes(0); expect(spyUnset).toHaveBeenCalledTimes(2); From 112a0f6cafdfde0f926e2d632caa45f9778c7fef Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 10:01:14 -0800 Subject: [PATCH 62/68] revert package.version to 1.3.4 --- packages/browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 6bb383f2..4d351e66 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/experiment-js-client", - "version": "1.4.0", + "version": "1.3.4", "description": "Javascript Client SDK for Amplitude Experiment", "main": "dist/experiment.umd.js", "types": "dist/types/src/index.d.ts", From 32511bba7106ae3bc4e73cf2f6d0025067cf6571 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 11:16:59 -0800 Subject: [PATCH 63/68] add exposure method to manually track exposures --- packages/browser/src/experimentClient.ts | 184 ++++++++++++----------- 1 file changed, 99 insertions(+), 85 deletions(-) diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index df0fedbc..af9881fb 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -137,96 +137,20 @@ export class ExperimentClient implements Client { } const { source, variant } = this.variantAndSource(key, fallback); if (this.config.automaticClientSideExposureTracking) { - if (isFallback(source) || !variant?.value) { - // fallbacks indicate not being allocated into an experiment, so - // we can unset the property - const user = this.addContext(this.getUser()); - const event = exposureEvent(user, key, variant, source); - this.analyticsProvider?.unsetUserProperty?.(event); - } else if (variant?.value) { - // fallbacks indicate not being allocated into an experiment, so - // we can unset the property - // only track when there's a value for a non fallback variant - const user = this.addContext(this.getUser()); - const event = exposureEvent(user, key, variant, source); - this.analyticsProvider?.setUserProperty?.(event); - this.analyticsProvider?.track(event); - } + this.exposureInternal(key, variant, source); } this.debug(`[Experiment] variant for ${key} is ${variant.value}`); return variant; } - private variantAndSource( - key: string, - fallback: string | Variant, - ): { - variant: Variant; - source: VariantSource; - } { - if (this.config.source === Source.InitialVariants) { - // for source = InitialVariants, fallback order goes: - // 1. InitialFlags - // 2. Local Storage - // 3. Function fallback - // 4. Config fallback - - const sourceVariant = this.sourceVariants()[key]; - if (!isNullOrUndefined(sourceVariant)) { - return { - variant: this.convertVariant(sourceVariant), - source: VariantSource.InitialVariants, - }; - } - const secondaryVariant = this.secondaryVariants()[key]; - if (!isNullOrUndefined(secondaryVariant)) { - return { - variant: this.convertVariant(secondaryVariant), - source: VariantSource.SecondaryLocalStoraage, - }; - } - if (!isNullOrUndefined(fallback)) { - return { - variant: this.convertVariant(fallback), - source: VariantSource.FallbackInline, - }; - } - return { - variant: this.convertVariant(this.config.fallbackVariant), - source: VariantSource.FallbackConfig, - }; - } else { - // for source = LocalStorage, fallback order goes: - // 1. Local Storage - // 2. Function fallback - // 3. InitialFlags - // 4. Config fallback - - const sourceVariant = this.sourceVariants()[key]; - if (!isNullOrUndefined(sourceVariant)) { - return { - variant: this.convertVariant(sourceVariant), - source: VariantSource.LocalStorage, - }; - } - if (!isNullOrUndefined(fallback)) { - return { - variant: this.convertVariant(fallback), - source: VariantSource.FallbackInline, - }; - } - const secondaryVariant = this.secondaryVariants()[key]; - if (!isNullOrUndefined(secondaryVariant)) { - return { - variant: this.convertVariant(secondaryVariant), - source: VariantSource.SecondaryInitialVariants, - }; - } - return { - variant: this.convertVariant(this.config.fallbackVariant), - source: VariantSource.FallbackConfig, - }; - } + /** + * Track an exposure event for the variant associated with the flag/experiment + * {@link key}. + * @param key The flag/experiment key to track an exposure for. + */ + public exposure(key: string): void { + const { source, variant } = this.variantAndSource(key, null); + this.exposureInternal(key, variant, source); } /** @@ -305,6 +229,78 @@ export class ExperimentClient implements Client { return this; } + private variantAndSource( + key: string, + fallback: string | Variant, + ): { + variant: Variant; + source: VariantSource; + } { + if (this.config.source === Source.InitialVariants) { + // for source = InitialVariants, fallback order goes: + // 1. InitialFlags + // 2. Local Storage + // 3. Function fallback + // 4. Config fallback + + const sourceVariant = this.sourceVariants()[key]; + if (!isNullOrUndefined(sourceVariant)) { + return { + variant: this.convertVariant(sourceVariant), + source: VariantSource.InitialVariants, + }; + } + const secondaryVariant = this.secondaryVariants()[key]; + if (!isNullOrUndefined(secondaryVariant)) { + return { + variant: this.convertVariant(secondaryVariant), + source: VariantSource.SecondaryLocalStoraage, + }; + } + if (!isNullOrUndefined(fallback)) { + return { + variant: this.convertVariant(fallback), + source: VariantSource.FallbackInline, + }; + } + return { + variant: this.convertVariant(this.config.fallbackVariant), + source: VariantSource.FallbackConfig, + }; + } else { + // for source = LocalStorage, fallback order goes: + // 1. Local Storage + // 2. Function fallback + // 3. InitialFlags + // 4. Config fallback + + const sourceVariant = this.sourceVariants()[key]; + if (!isNullOrUndefined(sourceVariant)) { + return { + variant: this.convertVariant(sourceVariant), + source: VariantSource.LocalStorage, + }; + } + if (!isNullOrUndefined(fallback)) { + return { + variant: this.convertVariant(fallback), + source: VariantSource.FallbackInline, + }; + } + const secondaryVariant = this.secondaryVariants()[key]; + if (!isNullOrUndefined(secondaryVariant)) { + return { + variant: this.convertVariant(secondaryVariant), + source: VariantSource.SecondaryInitialVariants, + }; + } + return { + variant: this.convertVariant(this.config.fallbackVariant), + source: VariantSource.FallbackConfig, + }; + } + } + private async fetchInternal( user: ExperimentUser, timeoutMillis: number, @@ -459,6 +455,24 @@ export class ExperimentClient implements Client { } } + private exposureInternal( + key: string, + variant: Variant, + source: VariantSource, + ): void { + const user = this.addContext(this.getUser()); + const event = exposureEvent(user, key, variant, source); + if (isFallback(source) || !variant?.value) { + // fallbacks indicate not being allocated into an experiment, so + // we can unset the property + this.analyticsProvider?.unsetUserProperty?.(event); + } else if (variant?.value) { + // only track when there's a value for a non fallback variant + this.analyticsProvider?.setUserProperty?.(event); + this.analyticsProvider?.track(event); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private debug(message?: any, ...optionalParams: any[]): void { if (this.config.debug) { From 12ad9593d3f29c3a71658858147cd0973a198365 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 13:38:31 -0800 Subject: [PATCH 64/68] add exposure function to client itface --- packages/browser/src/experimentClient.ts | 6 ++++++ packages/browser/src/types/client.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/browser/src/experimentClient.ts b/packages/browser/src/experimentClient.ts index af9881fb..c4fb3eee 100644 --- a/packages/browser/src/experimentClient.ts +++ b/packages/browser/src/experimentClient.ts @@ -146,6 +146,12 @@ export class ExperimentClient implements Client { /** * Track an exposure event for the variant associated with the flag/experiment * {@link key}. + * + * This method requires that an {@link ExperimentAnalyticsProvider} be + * configured when this client is initialized, either manually, or through the + * Amplitude Analytics SDK integration from set up using + * {@link Experiment.initializeWithAmplitudeAnalytics}. + * * @param key The flag/experiment key to track an exposure for. */ public exposure(key: string): void { diff --git a/packages/browser/src/types/client.ts b/packages/browser/src/types/client.ts index 1932d39b..2f2589c4 100644 --- a/packages/browser/src/types/client.ts +++ b/packages/browser/src/types/client.ts @@ -10,6 +10,7 @@ export interface Client { fetch(user?: ExperimentUser): Promise; variant(key: string, fallback?: string | Variant): Variant; all(): Variants; + exposure(key: string): void; getUser(): ExperimentUser; setUser(user: ExperimentUser): void; From 1e406eb4fd82f7d4a2eb5794786c39186a04ecbd Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 14:10:30 -0800 Subject: [PATCH 65/68] fix lint --- packages/core/src/amplitudeCore.ts | 3 ++- packages/core/src/applicationContextProvider.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/amplitudeCore.ts b/packages/core/src/amplitudeCore.ts index 21964d7d..5e37a8a7 100644 --- a/packages/core/src/amplitudeCore.ts +++ b/packages/core/src/amplitudeCore.ts @@ -8,7 +8,8 @@ safeGlobal['amplitudeCoreInstances'] = {}; export class AmplitudeCore { public readonly identityStore = new IdentityStoreImpl(); public readonly analyticsConnector = new AnalyticsConnectorImpl(); - public readonly applicationContextProvider = new ApplicationContextProviderImpl(); + public readonly applicationContextProvider = + new ApplicationContextProviderImpl(); static getInstance(instanceName: string): AmplitudeCore { if (!safeGlobal['amplitudeCoreInstances'][instanceName]) { diff --git a/packages/core/src/applicationContextProvider.ts b/packages/core/src/applicationContextProvider.ts index ed1991a9..1b94dda5 100644 --- a/packages/core/src/applicationContextProvider.ts +++ b/packages/core/src/applicationContextProvider.ts @@ -14,7 +14,8 @@ export interface ApplicationContextProvider { } export class ApplicationContextProviderImpl - implements ApplicationContextProvider { + implements ApplicationContextProvider +{ private readonly ua = new UAParser(navigator.userAgent).getResult(); public versionName: string; getApplicationContext(): ApplicationContext { From 03d3fd04d0c7f6812c0a1f0eb900b2ee752ae16d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 14:13:25 -0800 Subject: [PATCH 66/68] add method to stub --- packages/browser/src/stubClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/browser/src/stubClient.ts b/packages/browser/src/stubClient.ts index caa1c1fe..eb9973d2 100644 --- a/packages/browser/src/stubClient.ts +++ b/packages/browser/src/stubClient.ts @@ -37,4 +37,6 @@ export class StubExperimentClient implements Client { public all(): Variants { return {}; } + + public exposure(key: string): void {} } From a5884be608235702767d4582b2edeac7ac0ae79a Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 14:22:58 -0800 Subject: [PATCH 67/68] fix lint --- packages/browser/test/base64.test.ts | 25 ++----------------------- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/browser/test/base64.test.ts b/packages/browser/test/base64.test.ts index 2b786d76..31448c5f 100644 --- a/packages/browser/test/base64.test.ts +++ b/packages/browser/test/base64.test.ts @@ -2,29 +2,8 @@ import { stringToUtf8Array, urlSafeBase64Encode } from '../src/util/base64'; test('stringToUtf8Array', () => { expect(stringToUtf8Array('My 🚀 is full of 🦎')).toEqual([ - 77, - 121, - 32, - 240, - 159, - 154, - 128, - 32, - 105, - 115, - 32, - 102, - 117, - 108, - 108, - 32, - 111, - 102, - 32, - 240, - 159, - 166, - 142, + 77, 121, 32, 240, 159, 154, 128, 32, 105, 115, 32, 102, 117, 108, 108, 32, + 111, 102, 32, 240, 159, 166, 142, ]); }); diff --git a/yarn.lock b/yarn.lock index 760c7a63..8c30b652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,7 +3,7 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.4.0" + version "1.3.4" dependencies: "@amplitude/amplitude-core" "1.0.0" base64-js "1.5.1" From 48644a1fae5541cdbf66adb8331c43bb194f4b3f Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jan 2022 14:33:48 -0800 Subject: [PATCH 68/68] try to fix build for node v10 --- package.json | 3 ++ yarn.lock | 80 +++++++++------------------------------------------- 2 files changed, 16 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index f2f8fdd5..ad5d3c45 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,8 @@ "tslib": "^2.0.1", "typedoc": "^0.20.32", "typescript": "^3.9.7" + }, + "resolutions": { + "@testing-library/dom": "7.26.0" } } diff --git a/yarn.lock b/yarn.lock index 8c30b652..4825b0d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1202,7 +1202,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== @@ -2641,11 +2641,6 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@sheerun/mutationobserver-shim@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" - integrity sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw== - "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -2763,32 +2758,19 @@ "@svgr/plugin-svgo" "^4.3.1" loader-utils "^1.2.3" -"@testing-library/dom@*": - version "8.11.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.3.tgz#38fd63cbfe14557021e88982d931e33fb7c1a808" - integrity sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA== +"@testing-library/dom@*", "@testing-library/dom@7.26.0", "@testing-library/dom@^6.15.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.0.tgz#da4d052dc426a4ccc916303369c6e7552126f680" + integrity sha512-fyKFrBbS1IigaE3FV21LyeC7kSGF84lqTlSYdKmGaHuK2eYQ/bXVPM5vAa2wx/AU1iPD6oQHsxy2QQ17q9AMCg== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" + "@babel/runtime" "^7.10.3" "@types/aria-query" "^4.2.0" - aria-query "^5.0.0" + aria-query "^4.2.2" chalk "^4.1.0" - dom-accessibility-api "^0.5.9" + dom-accessibility-api "^0.5.1" lz-string "^1.4.4" - pretty-format "^27.0.2" - -"@testing-library/dom@^6.15.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.16.0.tgz#04ada27ed74ad4c0f0d984a1245bb29b1fd90ba9" - integrity sha512-lBD88ssxqEfz0wFL6MeUyyWZfV/2cjEZZV3YRpb2IoJRej/4f1jB0TzqIOznTpfR1r34CNesrubxwIlAQ8zgPA== - dependencies: - "@babel/runtime" "^7.8.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - "@types/testing-library__dom" "^6.12.1" - aria-query "^4.0.2" - dom-accessibility-api "^0.3.0" - pretty-format "^25.1.0" - wait-for-expect "^3.0.2" + pretty-format "^26.4.2" "@testing-library/jest-dom@^4.2.4": version "4.2.4" @@ -3032,13 +3014,6 @@ dependencies: "@testing-library/dom" "*" -"@types/testing-library__dom@^6.12.1": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e" - integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA== - dependencies: - pretty-format "^24.3.0" - "@types/testing-library__react@^9.1.2": version "9.1.3" resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.3.tgz#35eca61cc6ea923543796f16034882a1603d7302" @@ -3622,11 +3597,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -3681,7 +3651,7 @@ aria-query@^3.0.0: ast-types-flow "0.0.7" commander "^2.11.0" -aria-query@^4.0.2: +aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== @@ -3689,11 +3659,6 @@ aria-query@^4.0.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" -aria-query@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" - integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== - arity-n@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" @@ -5940,12 +5905,7 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz#511e5993dd673b97c87ea47dba0e3892f7e0c983" - integrity sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA== - -dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.1: version "0.5.10" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c" integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== @@ -12312,7 +12272,7 @@ pretty-error@^2.1.1: lodash "^4.17.20" renderkid "^2.0.4" -pretty-format@^24.0.0, pretty-format@^24.3.0, pretty-format@^24.9.0: +pretty-format@^24.0.0, pretty-format@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== @@ -12332,7 +12292,7 @@ pretty-format@^25.1.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.0.0, pretty-format@^26.6.2: +pretty-format@^26.0.0, pretty-format@^26.4.2, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -12342,15 +12302,6 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -pretty-format@^27.0.2: - version "27.4.6" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" - integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -15139,11 +15090,6 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -wait-for-expect@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463" - integrity sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag== - walker@^1.0.7, walker@~1.0.5: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"