Skip to content

Commit

Permalink
feat: Page targeting for Web Experimentation (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiuhc authored Aug 22, 2024
1 parent a9e47dc commit ab4ee1f
Show file tree
Hide file tree
Showing 14 changed files with 454 additions and 495 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ dist/
# For CI to ignore .npmrc file when publishing
.npmrc

# Example Experiment tag script example
# Example Experiment tag script
packages/experiment-tag/example/
3 changes: 3 additions & 0 deletions packages/analytics-connector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"bugs": {
"url": "https://github.com/amplitude/experiment-js-client/issues"
},
"dependencies": {
"@amplitude/experiment-core": "^0.8.0"
},
"devDependencies": {
"@types/amplitude-js": "^8.0.2",
"amplitude-js": "^8.12.0"
Expand Down
52 changes: 46 additions & 6 deletions packages/analytics-connector/src/eventBridge.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
getGlobalScope,
isLocalStorageAvailable,
} from '@amplitude/experiment-core';

export type AnalyticsEvent = {
eventType: string;
eventProperties?: Record<string, unknown>;
Expand All @@ -8,17 +13,47 @@ export type AnalyticsEventReceiver = (event: AnalyticsEvent) => void;

export interface EventBridge {
logEvent(event: AnalyticsEvent): void;

setEventReceiver(listener: AnalyticsEventReceiver): void;

setInstanceName(instanceName: string): void;
}

export class EventBridgeImpl implements EventBridge {
private instanceName = '';
private receiver: AnalyticsEventReceiver;
private queue: AnalyticsEvent[] = [];
private inMemoryQueue: AnalyticsEvent[] = [];
private globalScope = getGlobalScope();

private getStorageKey(): string {
return `EXP_unsent_${this.instanceName}`;
}

private getQueue(): AnalyticsEvent[] {
if (isLocalStorageAvailable()) {
const storageKey = this.getStorageKey();
const storedQueue = this.globalScope.localStorage.getItem(storageKey);
this.inMemoryQueue = storedQueue ? JSON.parse(storedQueue) : [];
}
return this.inMemoryQueue;
}

private setQueue(queue: AnalyticsEvent[]): void {
this.inMemoryQueue = queue;
if (isLocalStorageAvailable()) {
this.globalScope.localStorage.setItem(
this.getStorageKey(),
JSON.stringify(queue),
);
}
}

logEvent(event: AnalyticsEvent): void {
if (!this.receiver) {
if (this.queue.length < 512) {
this.queue.push(event);
const queue = this.getQueue();
if (queue.length < 512) {
queue.push(event);
this.setQueue(queue);
}
} else {
this.receiver(event);
Expand All @@ -27,11 +62,16 @@ export class EventBridgeImpl implements EventBridge {

setEventReceiver(receiver: AnalyticsEventReceiver): void {
this.receiver = receiver;
if (this.queue.length > 0) {
this.queue.forEach((event) => {
const queue = this.getQueue();
if (queue.length > 0) {
queue.forEach((event) => {
receiver(event);
});
this.queue = [];
this.setQueue([]);
}
}

public setInstanceName(instanceName: string): void {
this.instanceName = instanceName;
}
}
1 change: 1 addition & 0 deletions packages/experiment-browser/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const initializeWithAmplitudeAnalytics = (
const instanceKey = `${instanceName}.${apiKey}`;
const connector = AnalyticsConnector.getInstance(instanceName);
if (!instances[instanceKey]) {
connector.eventBridge.setInstanceName(instanceName);
config = {
userProvider: new DefaultUserProvider(
connector.applicationContextProvider,
Expand Down
9 changes: 8 additions & 1 deletion packages/experiment-browser/src/util/convert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EvaluationVariant } from '@amplitude/experiment-core';
import { EvaluationVariant, getGlobalScope } from '@amplitude/experiment-core';

import { ExperimentUser } from '../types/user';
import { Variant } from '../types/variant';
Expand All @@ -10,6 +10,13 @@ export const convertUserToContext = (
return {};
}
const context: Record<string, unknown> = { user: user };
// add page context
const globalScope = getGlobalScope();
if (globalScope) {
context.page = {
url: globalScope.location.href,
};
}
const groups: Record<string, Record<string, unknown>> = {};
if (!user.groups) {
return context;
Expand Down
6 changes: 6 additions & 0 deletions packages/experiment-browser/test/convert.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as util from '@amplitude/experiment-core';

import { ExperimentUser } from '../src/types/user';
import { convertUserToContext } from '../src/util/convert';

describe('convertUserToContext', () => {
beforeEach(() => {
jest.spyOn(util, 'getGlobalScope').mockReturnValue(undefined);
});

describe('groups', () => {
test('undefined user', () => {
const user: ExperimentUser | undefined = undefined;
Expand Down
6 changes: 5 additions & 1 deletion packages/experiment-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ export {
export { FlagApi, SdkFlagApi, GetFlagsOptions } from './api/flag-api';
export { HttpClient, HttpRequest, HttpResponse } from './transport/http';
export { Poller } from './util/poller';
export { safeGlobal } from './util/global';
export {
safeGlobal,
getGlobalScope,
isLocalStorageAvailable,
} from './util/global';
export { FetchError } from './evaluation/error';
31 changes: 31 additions & 0 deletions packages/experiment-core/src/util/global.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
export const safeGlobal =
typeof globalThis !== 'undefined' ? globalThis : global || self;

export const getGlobalScope = (): typeof globalThis | undefined => {
if (typeof globalThis !== 'undefined') {
return globalThis;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof self !== 'undefined') {
return self;
}
if (typeof global !== 'undefined') {
return global;
}
return undefined;
};

export const isLocalStorageAvailable = (): boolean => {
const globalScope = getGlobalScope();
if (globalScope) {
try {
const testKey = 'EXP_test';
globalScope.localStorage.setItem(testKey, testKey);
globalScope.localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
return false;
};
1 change: 1 addition & 0 deletions packages/experiment-tag/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"url": "https://github.com/amplitude/experiment-js-client/issues"
},
"dependencies": {
"@amplitude/experiment-core": "^0.8.0",
"@amplitude/experiment-js-client": "^1.11.0",
"dom-mutator": "^0.6.0"
},
Expand Down
Loading

0 comments on commit ab4ee1f

Please sign in to comment.