Skip to content
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: add global user properties plugin #577

Merged
merged 15 commits into from
Sep 18, 2023
1 change: 1 addition & 0 deletions packages/analytics-types/src/base-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IngestionMetadataEventProperty } from './ingestion-metadata';

export interface BaseEvent extends EventOptions {
event_type: string;
global_user_properties?: { [key: string]: any } | undefined;
event_properties?: { [key: string]: any } | undefined;
user_properties?: { [key: string]: any } | undefined;
group_properties?: { [key: string]: any } | undefined;
Expand Down
13 changes: 8 additions & 5 deletions packages/analytics-types/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export interface IdentifyUserProperties {
[IdentifyOperation.REMOVE]?: BaseOperationConfig;
}

export type UserProperties =
| IdentifyUserProperties
| {
[key in Exclude<string, IdentifyOperation>]: any;
};

export interface Revenue {
getEventProperties(): RevenueEventProperties;
setProductId(productId: string): Revenue;
Expand Down Expand Up @@ -107,11 +113,8 @@ export interface TrackEvent extends BaseEvent {

export interface IdentifyEvent extends BaseEvent {
event_type: SpecialEventType.IDENTIFY;
user_properties:
| IdentifyUserProperties
| {
[key in Exclude<string, IdentifyOperation>]: any;
};
global_user_properties?: UserProperties;
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved
user_properties: UserProperties;
}

export interface GroupIdentifyEvent extends BaseEvent {
Expand Down
Empty file.
96 changes: 96 additions & 0 deletions packages/plugin-global-user-properties/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<p align="center">
<a href="https://amplitude.com" target="_blank" align="center">
<img src="https://static.amplitude.com/lightning/46c85bfd91905de8047f1ee65c7c93d6fa9ee6ea/static/media/amplitude-logo-with-text.4fb9e463.svg" width="280">
</a>
<br />
</p>

# @amplitude/plugin-web-attribution-browser
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved

Official Browser SDK plugin for web attribution tracking

## Installation

This package is published on NPM registry and is available to be installed using npm and yarn.

```sh
# npm
npm install @amplitude/plugin-web-attribution-browser

# yarn
yarn add @amplitude/plugin-web-attribution-browser
```

## Usage

This plugin works on top of Amplitude Browser SDK and adds web attribution tracking features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v2.0.0` or later.

### 1. Import Amplitude packages

* `@amplitude/plugin-web-attribution-browser`

```typescript
import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser';
```

### 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 webAttributionTracking = webAttributionPlugin(amplitude, {
excludeReferrers: undefined,
initialEmptyValue: undefined,
resetSessionOnNewCampaign: undefined,
});
```

#### Options

|Name|Type|Default|Description|
|-|-|-|-|
|`excludeReferrers`|`(string \| RegExp)[]`|`[]`|Use this option to prevent the plugin from tracking campaigns parameters from specific referrers. For example: `subdomain.domain.com`.|
|`initialEmptyValue`|`string`|`"EMPTY"`|Use this option to specify empty values for [first-touch attribution](https://www.docs.developers.amplitude.com/data/sdks/marketing-analytics-browser/#first-touch-attribution).|
|`resetSessionOnNewCampaign`|`boolean`|`false`|Use this option to control whether a new session should start on a new campaign.|

### 3. Install plugin to Amplitude SDK

```typescript
amplitude.add(webAttributionTracking);
```

### 4. Initialize Amplitude SDK

```typescript
amplitude.init('API_KEY');
```

## Resulting web attribution event

This plugin tracks campaign parameters based on your configuration. A web attribution event is composed of the following values:

#### Event type
* `"$idenfity"`

#### User properties

|Property|Description|
|-|-|
|`utm_source`|URL query parameter value for `utm_source`|
|`utm_medium`|URL query parameter value for `utm_medium`|
|`utm_campaign`|URL query parameter value for `utm_campaign`|
|`utm_term`|URL query parameter value for `utm_term`|
|`utm_content`|URL query parameter value for `utm_content`|
|`referrer`|Referring webstite or `document.referrer`|
|`referring_domain`|Referring website's domain, including subdomain|
|`dclid`|URL query parameter value for `dclid`|
|`gbraid`|URL query parameter value for `gbraid`|
|`gclid`|URL query parameter value for `gclid`|
|`fbclid`|URL query parameter value for `fbclid`|
|`ko_click_id`|URL query parameter value for `ko_click_id`|
|`li_fat_id`|URL query parameter value for `li_fat_id`|
|`msclkid`|URL query parameter value for `msclkid`|
|`rtd_cid`|URL query parameter value for `rtd_cid`|
|`ttclid`|URL query parameter value for `ttclid`|
|`twclid`|URL query parameter value for `twclid`|
|`wbraid`|URL query parameter value for `wbraid`|
10 changes: 10 additions & 0 deletions packages/plugin-global-user-properties/jest.config.js
Original file line number Diff line number Diff line change
@@ -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'],
};
55 changes: 55 additions & 0 deletions packages/plugin-global-user-properties/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@amplitude/plugin-global-user-properties",
"version": "2.1.0",
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved
"description": "",
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved
"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": {
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved
"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"
]
}
3 changes: 3 additions & 0 deletions packages/plugin-global-user-properties/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { umd } from '../../scripts/build/rollup.config';

export default [umd];
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EnrichmentPlugin, Event } from '@amplitude/analytics-types';
import { GlobalUserPropertiesPlugin, Options } from './typings/global-user-properties';
import { isAmplitudeIdentifyEvent, isNotSpecialAmplitudeEvent } 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<Event> => {
if (isNotSpecialAmplitudeEvent(event) || isAmplitudeIdentifyEvent(event)) {
kelvin-lu marked this conversation as resolved.
Show resolved Hide resolved
event.global_user_properties = event.user_properties;

if (!options.shouldKeepOriginalUserProperties) {
delete event.user_properties;
}
}

return event;
},
};

return plugin;
};
11 changes: 11 additions & 0 deletions packages/plugin-global-user-properties/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Event, BaseEvent, SpecialEventType, IdentifyEvent } from '@amplitude/analytics-types';

const specialAmplitudeEvents = new Set(Object.values(SpecialEventType));

export const isNotSpecialAmplitudeEvent = (event: Event): event is BaseEvent => {
return !specialAmplitudeEvents.has(event.event_type as SpecialEventType);
};

export const isAmplitudeIdentifyEvent = (event: Event): event is IdentifyEvent => {
return event.event_type === SpecialEventType.IDENTIFY;
};
3 changes: 3 additions & 0 deletions packages/plugin-global-user-properties/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { globalUserPropertiesPlugin } from '../src/global-user-properties';
import { BaseEvent, IdentifyEvent, RevenueEvent, SpecialEventType } from '@amplitude/analytics-types';

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: BaseEvent = {
event_type: 'NOT A REAL EVENT TYPE',
user_properties: TEST_USER_PROPERTIES,
};

const newEvent = 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 = 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 = 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 = await plugin.execute?.({ ...event });

expect(newEvent?.global_user_properties).toStrictEqual(TEST_USER_IDENTIFY_PROPERTIES);
expect(newEvent?.user_properties).toStrictEqual(TEST_USER_IDENTIFY_PROPERTIES);
});
});
10 changes: 10 additions & 0 deletions packages/plugin-global-user-properties/tsconfig.es5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*"],
"compilerOptions": {
"module": "commonjs",
"noEmit": false,
"outDir": "lib/cjs",
"rootDir": "./src"
}
}
10 changes: 10 additions & 0 deletions packages/plugin-global-user-properties/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*"],
"compilerOptions": {
"module": "es6",
"noEmit": false,
"outDir": "lib/esm",
"rootDir": "./src"
}
}
11 changes: 11 additions & 0 deletions packages/plugin-global-user-properties/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*", "test/**/*"],
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"lib": ["dom"],
"noEmit": true,
"rootDir": ".",
}
}