-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: core package for seamless integration with amplitude analytics #15
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
c181d36
initial impl; need update logic, tests, deployment
bgiori 0de477c
fix export, add test files
bgiori cc25e5a
add basic test case
bgiori 1c8b8d9
fix lint, add update user properties logic; TODO Tests
bgiori ace689b
complete tests
bgiori ecda2d4
add amplitude-js as dev dependencies and clean up redeclared any
bgiori f3fbbfc
core instances be stored in global var
bgiori 8e8638d
commented code for consumer/producer
bgiori 0f40f3c
remove provider consumer
bgiori fda7083
rename factory method
bgiori 2b7ba13
update test
bgiori b0c2a90
use static init
bgiori 363aa5f
fix build
bgiori 9bb2b92
update rollup
bgiori a90cd88
remove unecessary dev deps
bgiori ff60d84
copy user properties when editing identity to call listener
bgiori b3c0897
0.0.2
bgiori 91f6987
0.0.3
bgiori ca7f2ff
fix core tests
bgiori 5f04d68
add max queue size
bgiori c522ed2
add app context provider to core
bgiori cf214a1
1.0.0-alpha.0
bgiori c9ffcc7
remove non-idempotent id operations
bgiori ba74d7f
v1.0.0
bgiori ed9f230
remove unecessary dev dependencies
bgiori 5a394b3
undo browser changes
bgiori 9a4becc
actually undo last undo
bgiori 227c262
Merge branch 'main' into core
bgiori 3218c5d
fix lint
bgiori 7591915
add ua parser js as a dependency
bgiori File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,2 @@ | ||
dist/ | ||
*.md | ||
*.test.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: '<rootDir>/', | ||
}), | ||
globals: { | ||
'ts-jest': { | ||
tsconfig: '<rootDir>/tsconfig.test.json', | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) { | ||
bgiori marked this conversation as resolved.
Show resolved
Hide resolved
|
||
safeGlobal['amplitudeCoreInstances'][instanceName] = new AmplitudeCore(); | ||
} | ||
return safeGlobal['amplitudeCoreInstances'][instanceName]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export type AnalyticsEvent = { | ||
eventType: string; | ||
eventProperties?: Record<string, unknown>; | ||
userProperties?: Record<string, unknown>; | ||
}; | ||
|
||
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 = []; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) || | ||
'' | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, unknown>; | ||
}; | ||
|
||
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<string, unknown>): IdentityEditor; | ||
updateUserProperties( | ||
actions: Record<string, Record<string, unknown>>, | ||
): IdentityEditor; | ||
commit(): void; | ||
} | ||
|
||
export class IdentityStoreImpl implements IdentityStore { | ||
private identity: Identity = { userProperties: {} }; | ||
private listeners = new Set<IdentityListener>(); | ||
|
||
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<string, unknown>, | ||
): IdentityEditor { | ||
actingIdentity.userProperties = userProperties; | ||
return this; | ||
}, | ||
|
||
updateUserProperties: function ( | ||
actions: Record<string, Record<string, unknown>>, | ||
): 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; | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious is esm also supported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to add support for this later. High priority ATM but not blocking this release.