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/package.json b/package.json index 6d4c2922..f2f8fdd5 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/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..3071c3f6 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,38 @@ +{ + "name": "@amplitude/amplitude-core", + "version": "1.0.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", + "dependencies": { + "@amplitude/ua-parser-js": "0.7.26" + }, + "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 new file mode 100644 index 00000000..63a1f5c9 --- /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: '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 new file mode 100644 index 00000000..21964d7d --- /dev/null +++ b/packages/core/src/amplitudeCore.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..04f92536 --- /dev/null +++ b/packages/core/src/analyticsConnector.ts @@ -0,0 +1,37 @@ +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 new file mode 100644 index 00000000..ed1991a9 --- /dev/null +++ b/packages/core/src/applicationContextProvider.ts @@ -0,0 +1,48 @@ +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 new file mode 100644 index 00000000..c8038641 --- /dev/null +++ b/packages/core/src/identityStore.ts @@ -0,0 +1,161 @@ +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 new file mode 100644 index 00000000..f2487bd2 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..e399c678 --- /dev/null +++ b/packages/core/src/util/global.ts @@ -0,0 +1,2 @@ +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 new file mode 100644 index 00000000..9736f375 --- /dev/null +++ b/packages/core/test/amplitudeCore.test.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..430d1a00 --- /dev/null +++ b/packages/core/test/analyticsConnector.test.ts @@ -0,0 +1,31 @@ +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 new file mode 100644 index 00000000..ba29d4f2 --- /dev/null +++ b/packages/core/test/identityStore.test.ts @@ -0,0 +1,136 @@ +/* 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 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"] +} diff --git a/yarn.lock b/yarn.lock index 3219c339..126ae55d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,11 +3,34 @@ "@amplitude/experiment-js-client@file:packages/browser": - version "1.0.3" + version "1.3.4" 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/ua-parser-js@0.7.26": + version "0.7.26" + resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.26.tgz#18d889d84d2ba90c248ab6fcd7e3dd07f1c9c86e" + integrity sha512-62/Rid6YQ7F2KT/5vTre41Y26ivrEoFC8lbrsJZqBKaiXMJWG0YpNv9RgxNSaZS2jPLVQgoB/FFeWxihOLfIcg== + +"@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 +3098,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 +3824,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 +4469,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 +12671,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 +14779,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"