Skip to content

Commit 58446e2

Browse files
authored
feat: add integration plugin; segment plugin; web exp updates (#126)
1 parent 9d11786 commit 58446e2

34 files changed

+2398
-409
lines changed

packages/analytics-connector/src/analyticsConnector.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,6 @@ export class AnalyticsConnector {
1717
safeGlobal['analyticsConnectorInstances'][instanceName] =
1818
new AnalyticsConnector();
1919
}
20-
const instance = safeGlobal['analyticsConnectorInstances'][instanceName];
21-
// If the eventBridge is using old implementation, update with new instance
22-
if (!instance.eventBridge.setInstanceName) {
23-
const queue = instance.eventBridge.queue ?? [];
24-
const receiver = instance.eventBridge.receiver;
25-
instance.eventBridge = new EventBridgeImpl();
26-
instance.eventBridge.setInstanceName(instanceName);
27-
// handle case when receiver was not set during previous initialization
28-
if (receiver) {
29-
instance.eventBridge.setEventReceiver(receiver);
30-
}
31-
for (const event of queue) {
32-
instance.eventBridge.logEvent(event);
33-
}
34-
}
35-
return instance;
20+
return safeGlobal['analyticsConnectorInstances'][instanceName];
3621
}
3722
}
Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import {
2-
getGlobalScope,
3-
isLocalStorageAvailable,
4-
} from '@amplitude/experiment-core';
5-
61
export type AnalyticsEvent = {
72
eventType: string;
83
eventProperties?: Record<string, unknown>;
@@ -13,47 +8,17 @@ export type AnalyticsEventReceiver = (event: AnalyticsEvent) => void;
138

149
export interface EventBridge {
1510
logEvent(event: AnalyticsEvent): void;
16-
1711
setEventReceiver(listener: AnalyticsEventReceiver): void;
18-
19-
setInstanceName(instanceName: string): void;
2012
}
2113

2214
export class EventBridgeImpl implements EventBridge {
23-
private instanceName = '';
2415
private receiver: AnalyticsEventReceiver;
25-
private inMemoryQueue: AnalyticsEvent[] = [];
26-
private globalScope = getGlobalScope();
27-
28-
private getStorageKey(): string {
29-
return `EXP_unsent_${this.instanceName}`;
30-
}
31-
32-
private getQueue(): AnalyticsEvent[] {
33-
if (isLocalStorageAvailable()) {
34-
const storageKey = this.getStorageKey();
35-
const storedQueue = this.globalScope.localStorage.getItem(storageKey);
36-
this.inMemoryQueue = storedQueue ? JSON.parse(storedQueue) : [];
37-
}
38-
return this.inMemoryQueue;
39-
}
40-
41-
private setQueue(queue: AnalyticsEvent[]): void {
42-
this.inMemoryQueue = queue;
43-
if (isLocalStorageAvailable()) {
44-
this.globalScope.localStorage.setItem(
45-
this.getStorageKey(),
46-
JSON.stringify(queue),
47-
);
48-
}
49-
}
16+
private queue: AnalyticsEvent[] = [];
5017

5118
logEvent(event: AnalyticsEvent): void {
5219
if (!this.receiver) {
53-
const queue = this.getQueue();
54-
if (queue.length < 512) {
55-
queue.push(event);
56-
this.setQueue(queue);
20+
if (this.queue.length < 512) {
21+
this.queue.push(event);
5722
}
5823
} else {
5924
this.receiver(event);
@@ -62,16 +27,11 @@ export class EventBridgeImpl implements EventBridge {
6227

6328
setEventReceiver(receiver: AnalyticsEventReceiver): void {
6429
this.receiver = receiver;
65-
const queue = this.getQueue();
66-
if (queue.length > 0) {
67-
queue.forEach((event) => {
30+
if (this.queue.length > 0) {
31+
this.queue.forEach((event) => {
6832
receiver(event);
6933
});
70-
this.setQueue([]);
34+
this.queue = [];
7135
}
7236
}
73-
74-
public setInstanceName(instanceName: string): void {
75-
this.instanceName = instanceName;
76-
}
7737
}

packages/experiment-browser/src/experimentClient.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import {
1818
import { version as PACKAGE_VERSION } from '../package.json';
1919

2020
import { Defaults, ExperimentConfig } from './config';
21-
import { ConnectorUserProvider } from './integration/connector';
22-
import { DefaultUserProvider } from './integration/default';
21+
import { IntegrationManager } from './integration/manager';
2322
import {
2423
getFlagStorage,
2524
getVariantStorage,
@@ -31,6 +30,7 @@ import { FetchHttpClient, WrapperClient } from './transport/http';
3130
import { exposureEvent } from './types/analytics';
3231
import { Client, FetchOptions } from './types/client';
3332
import { Exposure, ExposureTrackingProvider } from './types/exposure';
33+
import { ExperimentPlugin, IntegrationPlugin } from './types/plugin';
3434
import { ExperimentUserProvider } from './types/provider';
3535
import { isFallback, Source, VariantSource } from './types/source';
3636
import { ExperimentUser } from './types/user';
@@ -84,6 +84,7 @@ export class ExperimentClient implements Client {
8484
flagPollerIntervalMillis,
8585
);
8686
private isRunning = false;
87+
private readonly integrationManager: IntegrationManager;
8788

8889
// Deprecated
8990
private analyticsProvider: SessionAnalyticsProvider | undefined;
@@ -136,6 +137,7 @@ export class ExperimentClient implements Client {
136137
this.config.exposureTrackingProvider,
137138
);
138139
}
140+
this.integrationManager = new IntegrationManager(this.config, this);
139141
// Setup Remote APIs
140142
const httpClient = new WrapperClient(
141143
this.config.httpClient || FetchHttpClient,
@@ -252,7 +254,9 @@ export class ExperimentClient implements Client {
252254
options,
253255
);
254256
} catch (e) {
255-
console.error(e);
257+
if (this.config.debug) {
258+
console.error(e);
259+
}
256260
}
257261
return this;
258262
}
@@ -677,7 +681,7 @@ export class ExperimentClient implements Client {
677681
timeoutMillis: number,
678682
options?: FetchOptions,
679683
): Promise<Variants> {
680-
user = await this.addContextOrWait(user, 10000);
684+
user = await this.addContextOrWait(user);
681685
user = this.cleanUserPropsForFetch(user);
682686
this.debug('[Experiment] Fetch variants for user: ', user);
683687
const results = await this.evaluationApi.getVariants(user, {
@@ -756,28 +760,25 @@ export class ExperimentClient implements Client {
756760

757761
private addContext(user: ExperimentUser): ExperimentUser {
758762
const providedUser = this.userProvider?.getUser();
763+
const integrationUser = this.integrationManager.getUser();
759764
const mergedUserProperties = {
760-
...user?.user_properties,
761765
...providedUser?.user_properties,
766+
...integrationUser.user_properties,
767+
...user?.user_properties,
762768
};
763769
return {
764770
library: `experiment-js-client/${PACKAGE_VERSION}`,
765-
...this.userProvider?.getUser(),
771+
...providedUser,
772+
...integrationUser,
766773
...user,
767774
user_properties: mergedUserProperties,
768775
};
769776
}
770777

771778
private async addContextOrWait(
772779
user: ExperimentUser,
773-
ms: number,
774780
): Promise<ExperimentUser> {
775-
if (this.userProvider instanceof DefaultUserProvider) {
776-
if (this.userProvider.userProvider instanceof ConnectorUserProvider) {
777-
await this.userProvider.userProvider.identityReady(ms);
778-
}
779-
}
780-
781+
await this.integrationManager.ready();
781782
return this.addContext(user);
782783
}
783784

@@ -798,6 +799,12 @@ export class ExperimentClient implements Client {
798799
}
799800

800801
private exposureInternal(key: string, sourceVariant: SourceVariant): void {
802+
// Variant metadata may disable exposure tracking remotely.
803+
const trackExposure =
804+
(sourceVariant.variant?.metadata?.trackExposure as boolean) ?? true;
805+
if (!trackExposure) {
806+
return;
807+
}
801808
this.legacyExposureInternal(
802809
key,
803810
sourceVariant.variant,
@@ -823,6 +830,7 @@ export class ExperimentClient implements Client {
823830
}
824831
if (metadata) exposure.metadata = metadata;
825832
this.exposureTrackingProvider?.track(exposure);
833+
this.integrationManager.track(exposure);
826834
}
827835

828836
private legacyExposureInternal(
@@ -855,6 +863,16 @@ export class ExperimentClient implements Client {
855863
}
856864
return true;
857865
}
866+
867+
/**
868+
* Add a plugin to the experiment client.
869+
* @param plugin the plugin to add.
870+
*/
871+
public addPlugin(plugin: ExperimentPlugin): void {
872+
if (plugin.type === 'integration') {
873+
this.integrationManager.setIntegration(plugin as IntegrationPlugin);
874+
}
875+
}
858876
}
859877

860878
type SourceVariant = {

packages/experiment-browser/src/factory.ts

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { AnalyticsConnector } from '@amplitude/analytics-connector';
22

33
import { Defaults, ExperimentConfig } from './config';
44
import { ExperimentClient } from './experimentClient';
5-
import {
6-
ConnectorExposureTrackingProvider,
7-
ConnectorUserProvider,
8-
} from './integration/connector';
9-
import { DefaultUserProvider } from './integration/default';
5+
import { AmplitudeIntegrationPlugin } from './integration/amplitude';
6+
import { DefaultUserProvider } from './providers/default';
107

118
const instances = {};
129

10+
const getInstanceName = (config: ExperimentConfig): string => {
11+
return config?.instanceName || Defaults.instanceName;
12+
};
13+
1314
/**
1415
* Initializes a singleton {@link ExperimentClient} identified by the configured
1516
* instance name.
@@ -23,17 +24,17 @@ const initialize = (
2324
): ExperimentClient => {
2425
// Store instances by appending the instance name and api key. Allows for
2526
// initializing multiple default instances for different api keys.
26-
const instanceName = config?.instanceName || Defaults.instanceName;
27-
const instanceKey = `${instanceName}.${apiKey}`;
28-
const connector = AnalyticsConnector.getInstance(instanceName);
27+
const instanceName = getInstanceName(config);
28+
// The internal instance name prefix is used by web experiment to differentiate
29+
// web and feature experiment sdks which use the same api key.
30+
const internalInstanceNameSuffix = config?.['internalInstanceNameSuffix'];
31+
const instanceKey = internalInstanceNameSuffix
32+
? `${instanceName}.${apiKey}.${internalInstanceNameSuffix}`
33+
: `${instanceName}.${apiKey}`;
2934
if (!instances[instanceKey]) {
3035
config = {
3136
...config,
32-
userProvider: new DefaultUserProvider(
33-
connector.applicationContextProvider,
34-
config?.userProvider,
35-
apiKey,
36-
),
37+
userProvider: new DefaultUserProvider(config?.userProvider, apiKey),
3738
};
3839
instances[instanceKey] = new ExperimentClient(apiKey, config);
3940
}
@@ -55,32 +56,16 @@ const initializeWithAmplitudeAnalytics = (
5556
apiKey: string,
5657
config?: ExperimentConfig,
5758
): ExperimentClient => {
58-
// Store instances by appending the instance name and api key. Allows for
59-
// initializing multiple default instances for different api keys.
60-
const instanceName = config?.instanceName || Defaults.instanceName;
61-
const instanceKey = `${instanceName}.${apiKey}`;
62-
const connector = AnalyticsConnector.getInstance(instanceName);
63-
if (!instances[instanceKey]) {
64-
connector.eventBridge.setInstanceName(instanceName);
65-
config = {
66-
userProvider: new DefaultUserProvider(
67-
connector.applicationContextProvider,
68-
new ConnectorUserProvider(connector.identityStore),
69-
apiKey,
70-
),
71-
exposureTrackingProvider: new ConnectorExposureTrackingProvider(
72-
connector.eventBridge,
73-
),
74-
...config,
75-
};
76-
instances[instanceKey] = new ExperimentClient(apiKey, config);
77-
if (config.automaticFetchOnAmplitudeIdentityChange) {
78-
connector.identityStore.addIdentityListener(() => {
79-
instances[instanceKey].fetch();
80-
});
81-
}
82-
}
83-
return instances[instanceKey];
59+
const instanceName = getInstanceName(config);
60+
const client = initialize(apiKey, config);
61+
client.addPlugin(
62+
new AmplitudeIntegrationPlugin(
63+
apiKey,
64+
AnalyticsConnector.getInstance(instanceName),
65+
10000,
66+
),
67+
);
68+
return client;
8469
};
8570

8671
/**

packages/experiment-browser/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export { ExperimentConfig } from './config';
99
export {
1010
AmplitudeUserProvider,
1111
AmplitudeAnalyticsProvider,
12-
} from './integration/amplitude';
12+
} from './providers/amplitude';
13+
export { AmplitudeIntegrationPlugin } from './integration/amplitude';
1314
export { Experiment } from './factory';
1415
export { StubExperimentClient } from './stubClient';
1516
export { ExperimentClient } from './experimentClient';
@@ -23,3 +24,9 @@ export { Source } from './types/source';
2324
export { ExperimentUser } from './types/user';
2425
export { Variant, Variants } from './types/variant';
2526
export { Exposure, ExposureTrackingProvider } from './types/exposure';
27+
export {
28+
ExperimentPlugin,
29+
IntegrationPlugin,
30+
ExperimentPluginType,
31+
ExperimentEvent,
32+
} from './types/plugin';

0 commit comments

Comments
 (0)