diff --git a/packages/analytics-types/src/event.ts b/packages/analytics-types/src/event.ts index 3bd456abb..83ee9bbf9 100644 --- a/packages/analytics-types/src/event.ts +++ b/packages/analytics-types/src/event.ts @@ -64,6 +64,12 @@ export interface IdentifyUserProperties { [IdentifyOperation.REMOVE]?: BaseOperationConfig; } +export type UserProperties = + | IdentifyUserProperties + | { + [key in Exclude]: any; + }; + export interface Revenue { getEventProperties(): RevenueEventProperties; setProductId(productId: string): Revenue; @@ -107,20 +113,12 @@ export interface TrackEvent extends BaseEvent { export interface IdentifyEvent extends BaseEvent { event_type: SpecialEventType.IDENTIFY; - user_properties: - | IdentifyUserProperties - | { - [key in Exclude]: any; - }; + user_properties: UserProperties; } export interface GroupIdentifyEvent extends BaseEvent { event_type: SpecialEventType.GROUP_IDENTIFY; - group_properties: - | IdentifyUserProperties - | { - [key in Exclude]: any; - }; + group_properties: UserProperties; } export interface RevenueEvent extends BaseEvent { diff --git a/packages/plugin-global-user-properties/CHANGELOG.md b/packages/plugin-global-user-properties/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-global-user-properties/README.md b/packages/plugin-global-user-properties/README.md new file mode 100644 index 000000000..df59ff5a0 --- /dev/null +++ b/packages/plugin-global-user-properties/README.md @@ -0,0 +1,66 @@ +

+ + + +
+

+ +# @amplitude/plugin-global-user-properties + +Official SDK plugin for adding global user properties to events + +## Installation + +This package is published on NPM registry and is available to be installed using npm and yarn. + +```sh +# npm +npm install @amplitude/plugin-global-user-properties + +# yarn +yarn add @amplitude/plugin-global-user-properties +``` + +## Usage + +This plugin works on top of the Amplitude SDK and sends user properties as global user properties, a beta feature for that allows projects to share user properties (i.e. user properties can become "global" across multiple projects). + +To use this plugin, you need to install `@amplitude/plugin-global-user-properties `v1.0.0` or later. + +### 1. Import Amplitude packages + +* `@amplitude/plugin-global-user-properties` + +```typescript +import * as Amplitude from '@amplitude/analytics-browser' +import { globalUserPropertiesPlugin } from '@amplitude/plugin-global-user-properties'; +``` + +### 2. Instantiate page view plugin + +The plugin accepts an optional parameter of type `Object` to configure the plugin based on your use case. + +```typescript +const globalUserPropertiesPlugin = globalUserPropertiesPlugin({ + shouldKeepOriginalUserProperties: true, +}); +``` + +#### Options + +|Name|Type|Default|Description| +|-|-|-|-| +|`shouldKeepOriginalUserProperties`|`boolean`| `false` | Use this option if you want the user properties to be sent along with the global user properties. Since global user properties do not appear in Data yet, this would allow indirect governance (by observing the same properties as regular user properties). | + +### 3. Install plugin to Amplitude SDK + +```typescript +amplitude.add(globalUserPropertiesPlugin); +``` + +### 4. Initialize Amplitude SDK + +```typescript +amplitude.init('API_KEY'); +``` + diff --git a/packages/plugin-global-user-properties/jest.config.js b/packages/plugin-global-user-properties/jest.config.js new file mode 100644 index 000000000..dc4094b18 --- /dev/null +++ b/packages/plugin-global-user-properties/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../jest.config.js'); +const package = require('./package'); + +module.exports = { + ...baseConfig, + displayName: package.name, + rootDir: '.', + testEnvironment: 'jsdom', + coveragePathIgnorePatterns: ['index.ts'], +}; diff --git a/packages/plugin-global-user-properties/package.json b/packages/plugin-global-user-properties/package.json new file mode 100644 index 000000000..5175fa6e7 --- /dev/null +++ b/packages/plugin-global-user-properties/package.json @@ -0,0 +1,55 @@ +{ + "name": "@amplitude/plugin-global-user-properties", + "version": "1.0.0", + "description": "An event enrichment plugin that adds the experimental global user properties field to events", + "author": "Amplitude Inc", + "homepage": "https://github.com/amplitude/Amplitude-TypeScript", + "license": "MIT", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" + }, + "scripts": { + "build": "yarn bundle && yarn build:es5 && yarn build:esm", + "bundle": "rollup --config rollup.config.js", + "build:es5": "tsc -p ./tsconfig.es5.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "clean": "rimraf node_modules lib coverage", + "fix": "yarn fix:eslint & yarn fix:prettier", + "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", + "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", + "lint": "yarn lint:eslint & yarn lint:prettier", + "lint:eslint": "eslint '{src,test}/**/*.ts'", + "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "test": "jest", + "typecheck": "tsc -p ./tsconfig.json" + }, + "bugs": { + "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" + }, + "dependencies": { + "@amplitude/analytics-types": "^2.1.2", + "tslib": "^2.4.1" + }, + "devDependencies": { + "@amplitude/analytics-browser": "^2.2.3", + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", + "rollup": "^2.79.1", + "rollup-plugin-execute": "^1.1.1", + "rollup-plugin-gzip": "^3.1.0", + "rollup-plugin-terser": "^7.0.2" + }, + "files": [ + "lib" + ] +} diff --git a/packages/plugin-global-user-properties/rollup.config.js b/packages/plugin-global-user-properties/rollup.config.js new file mode 100644 index 000000000..2718b91d7 --- /dev/null +++ b/packages/plugin-global-user-properties/rollup.config.js @@ -0,0 +1,3 @@ +import { umd } from '../../scripts/build/rollup.config'; + +export default [umd]; diff --git a/packages/plugin-global-user-properties/src/global-user-properties.ts b/packages/plugin-global-user-properties/src/global-user-properties.ts new file mode 100644 index 000000000..35303ec24 --- /dev/null +++ b/packages/plugin-global-user-properties/src/global-user-properties.ts @@ -0,0 +1,25 @@ +import { EnrichmentPlugin, Event } from '@amplitude/analytics-types'; +import { GlobalUserPropertiesPlugin, Options } from './typings/global-user-properties'; +import { isAmplitudeIdentifyEvent, isTrackEvent } from './helpers'; + +export const globalUserPropertiesPlugin: GlobalUserPropertiesPlugin = function (options: Options = {}) { + const plugin: EnrichmentPlugin = { + name: '@amplitude/plugin-global-user-properties', + type: 'enrichment', + + /* Note: The promise is because of the interface, not because this has any asynchronous behavior */ + execute: async (event: Event): Promise => { + if (isTrackEvent(event) || isAmplitudeIdentifyEvent(event)) { + event.global_user_properties = event.user_properties; + + if (!options.shouldKeepOriginalUserProperties) { + delete event.user_properties; + } + } + + return event; + }, + }; + + return plugin; +}; diff --git a/packages/plugin-global-user-properties/src/helpers.ts b/packages/plugin-global-user-properties/src/helpers.ts new file mode 100644 index 000000000..5bc3ff026 --- /dev/null +++ b/packages/plugin-global-user-properties/src/helpers.ts @@ -0,0 +1,11 @@ +import { Event, TrackEvent, SpecialEventType, IdentifyEvent } from '@amplitude/analytics-types'; + +const specialAmplitudeEvents = new Set(Object.values(SpecialEventType)); + +export const isTrackEvent = (event: Event): event is TrackEvent => { + return !specialAmplitudeEvents.has(event.event_type as SpecialEventType); +}; + +export const isAmplitudeIdentifyEvent = (event: Event): event is IdentifyEvent => { + return event.event_type === SpecialEventType.IDENTIFY; +}; diff --git a/packages/plugin-global-user-properties/src/index.ts b/packages/plugin-global-user-properties/src/index.ts new file mode 100644 index 000000000..6600552d2 --- /dev/null +++ b/packages/plugin-global-user-properties/src/index.ts @@ -0,0 +1,3 @@ +export { globalUserPropertiesPlugin } from './global-user-properties'; +export { globalUserPropertiesPlugin as plugin } from './global-user-properties'; +export { GlobalUserPropertiesPlugin, Options } from './typings/global-user-properties'; diff --git a/packages/plugin-global-user-properties/src/modules.d.ts b/packages/plugin-global-user-properties/src/modules.d.ts new file mode 100644 index 000000000..23b1251a3 --- /dev/null +++ b/packages/plugin-global-user-properties/src/modules.d.ts @@ -0,0 +1,12 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { TrackEvent, IdentifyEvent } from '@amplitude/analytics-types'; + +declare module '@amplitude/analytics-types' { + export interface TrackEvent { + global_user_properties?: { [key: string]: any } | undefined; + } + + export interface IdentifyEvent { + global_user_properties?: { [key: string]: any } | undefined; + } +} diff --git a/packages/plugin-global-user-properties/src/typings/global-user-properties.ts b/packages/plugin-global-user-properties/src/typings/global-user-properties.ts new file mode 100644 index 000000000..54b6905dd --- /dev/null +++ b/packages/plugin-global-user-properties/src/typings/global-user-properties.ts @@ -0,0 +1,12 @@ +import { EnrichmentPlugin } from '@amplitude/analytics-types'; + +export interface Options { + /** + * Whether or not the orignal user_properties field should be kept on the event + */ + shouldKeepOriginalUserProperties?: boolean; +} + +export interface GlobalUserPropertiesPlugin { + (options?: Options): EnrichmentPlugin; +} diff --git a/packages/plugin-global-user-properties/test/global-user-properties.test.ts b/packages/plugin-global-user-properties/test/global-user-properties.test.ts new file mode 100644 index 000000000..8485516cc --- /dev/null +++ b/packages/plugin-global-user-properties/test/global-user-properties.test.ts @@ -0,0 +1,73 @@ +import { globalUserPropertiesPlugin } from '../src/global-user-properties'; +import { TrackEvent, IdentifyEvent, RevenueEvent, SpecialEventType } from '@amplitude/analytics-types'; + +// ts-jest is having difficulty finding the module declaration types, so there are some any's +describe('globalUserPropertiesPlugin', () => { + const TEST_USER_PROPERTIES = { + USER_PROPERTY_ONE: 'TEST_VALUE_ONE', + }; + + const TEST_USER_IDENTIFY_PROPERTIES = { + $set: { + USER_PROPERTY_ONE: 'TEST_VALUE_ONE', + }, + }; + + test('adds global properties on regular events', async () => { + const plugin = globalUserPropertiesPlugin(); + + const event: TrackEvent = { + event_type: 'NOT A REAL EVENT TYPE', + user_properties: TEST_USER_PROPERTIES, + }; + + const newEvent: any = await plugin.execute?.({ ...event }); + + expect(newEvent?.event_type).toEqual(event.event_type); + expect(newEvent?.global_user_properties).toStrictEqual(TEST_USER_PROPERTIES); + expect(newEvent?.user_properties).toStrictEqual(undefined); + }); + + test('adds global properties on identify events', async () => { + const plugin = globalUserPropertiesPlugin(); + + const event: IdentifyEvent = { + event_type: SpecialEventType.IDENTIFY, + user_properties: TEST_USER_IDENTIFY_PROPERTIES, + }; + + const newEvent: any = await plugin.execute?.({ ...event }); + + expect(newEvent?.global_user_properties).toStrictEqual(TEST_USER_IDENTIFY_PROPERTIES); + expect(newEvent?.user_properties).toStrictEqual(undefined); + }); + + test('does not add global properties on revenue events', async () => { + const plugin = globalUserPropertiesPlugin(); + + const event: RevenueEvent = { + event_type: SpecialEventType.REVENUE, + revenue: 3, + event_properties: {}, + }; + + const newEvent: any = await plugin.execute?.({ ...event }); + + expect(newEvent?.global_user_properties).toStrictEqual(undefined); + expect(newEvent?.user_properties).toStrictEqual(event.user_properties); + }); + + test('adds global properties and user properties on identify events with shouldKeepOriginalUserProperties option', async () => { + const plugin = globalUserPropertiesPlugin({ shouldKeepOriginalUserProperties: true }); + + const event: IdentifyEvent = { + event_type: SpecialEventType.IDENTIFY, + user_properties: TEST_USER_IDENTIFY_PROPERTIES, + }; + + const newEvent: any = await plugin.execute?.({ ...event }); + + expect(newEvent?.global_user_properties).toStrictEqual(TEST_USER_IDENTIFY_PROPERTIES); + expect(newEvent?.user_properties).toStrictEqual(TEST_USER_IDENTIFY_PROPERTIES); + }); +}); diff --git a/packages/plugin-global-user-properties/tsconfig.es5.json b/packages/plugin-global-user-properties/tsconfig.es5.json new file mode 100644 index 000000000..77e041d3f --- /dev/null +++ b/packages/plugin-global-user-properties/tsconfig.es5.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "outDir": "lib/cjs", + "rootDir": "./src" + } +} diff --git a/packages/plugin-global-user-properties/tsconfig.esm.json b/packages/plugin-global-user-properties/tsconfig.esm.json new file mode 100644 index 000000000..bec981eee --- /dev/null +++ b/packages/plugin-global-user-properties/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "es6", + "noEmit": false, + "outDir": "lib/esm", + "rootDir": "./src" + } +} diff --git a/packages/plugin-global-user-properties/tsconfig.json b/packages/plugin-global-user-properties/tsconfig.json new file mode 100644 index 000000000..955dcce78 --- /dev/null +++ b/packages/plugin-global-user-properties/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["dom"], + "noEmit": true, + "rootDir": ".", + } +} diff --git a/yarn.lock b/yarn.lock index 33a296cca..2be390f76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,16 @@ resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.4.8.tgz#dd801303db2662bc51be7e0194eeb8bd72267c42" integrity sha512-dFW7c7Wb6Ng7vbmzwbaXZSpqfBx37ukamJV9ErFYYS8vGZK/Hkbt3M7fZHBI4WFU6CCwakr2ZXPme11uGPYWkQ== +"@amplitude/plugin-web-attribution-browser@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.0.9.tgz#0d24f2da764846601f59484a8651d58918ef89d7" + integrity sha512-QrNgieAEXEBbtnsxYzfeJl2U/5XwCCvO3Dg0hntAtnTdu1A3HlN5ItRtoHg0jGBbu4jpSbvag72UlkCotqJ+Yg== + dependencies: + "@amplitude/analytics-client-common" "^2.0.5" + "@amplitude/analytics-core" "^2.0.4" + "@amplitude/analytics-types" "^2.1.2" + tslib "^2.4.1" + "@amplitude/ua-parser-js@^0.7.31": version "0.7.31" resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.31.tgz#749bf7cb633cfcc7ff3c10805bad7c5f6fbdbc61"