Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions packages/analytics-connector/src/analyticsConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,6 @@ export class AnalyticsConnector {
safeGlobal['analyticsConnectorInstances'][instanceName] =
new AnalyticsConnector();
}
const instance = safeGlobal['analyticsConnectorInstances'][instanceName];
// If the eventBridge is using old implementation, update with new instance
if (!instance.eventBridge.setInstanceName) {
const queue = instance.eventBridge.queue ?? [];
const receiver = instance.eventBridge.receiver;
instance.eventBridge = new EventBridgeImpl();
instance.eventBridge.setInstanceName(instanceName);
// handle case when receiver was not set during previous initialization
if (receiver) {
instance.eventBridge.setEventReceiver(receiver);
}
for (const event of queue) {
instance.eventBridge.logEvent(event);
}
}
return instance;
return safeGlobal['analyticsConnectorInstances'][instanceName];
}
}
52 changes: 6 additions & 46 deletions packages/analytics-connector/src/eventBridge.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import {
getGlobalScope,
isLocalStorageAvailable,
} from '@amplitude/experiment-core';

export type AnalyticsEvent = {
eventType: string;
eventProperties?: Record<string, unknown>;
Expand All @@ -13,47 +8,17 @@ 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 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),
);
}
}
private queue: AnalyticsEvent[] = [];

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

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

public setInstanceName(instanceName: string): void {
this.instanceName = instanceName;
}
}
34 changes: 22 additions & 12 deletions packages/experiment-browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import {
import { version as PACKAGE_VERSION } from '../package.json';

import { Defaults, ExperimentConfig } from './config';
import { ConnectorUserProvider } from './integration/connector';
import { DefaultUserProvider } from './integration/default';
import { IntegrationManager } from './integration/manager';
import {
getFlagStorage,
getVariantStorage,
Expand All @@ -31,6 +30,7 @@ import { FetchHttpClient, WrapperClient } from './transport/http';
import { exposureEvent } from './types/analytics';
import { Client, FetchOptions } from './types/client';
import { Exposure, ExposureTrackingProvider } from './types/exposure';
import { ExperimentPlugin, IntegrationPlugin } from './types/plugin';
import { ExperimentUserProvider } from './types/provider';
import { isFallback, Source, VariantSource } from './types/source';
import { ExperimentUser } from './types/user';
Expand Down Expand Up @@ -84,6 +84,7 @@ export class ExperimentClient implements Client {
flagPollerIntervalMillis,
);
private isRunning = false;
private readonly integrationManager: IntegrationManager;

// Deprecated
private analyticsProvider: SessionAnalyticsProvider | undefined;
Expand Down Expand Up @@ -136,6 +137,7 @@ export class ExperimentClient implements Client {
this.config.exposureTrackingProvider,
);
}
this.integrationManager = new IntegrationManager(this.config, this);
// Setup Remote APIs
const httpClient = new WrapperClient(
this.config.httpClient || FetchHttpClient,
Expand Down Expand Up @@ -677,7 +679,7 @@ export class ExperimentClient implements Client {
timeoutMillis: number,
options?: FetchOptions,
): Promise<Variants> {
user = await this.addContextOrWait(user, 10000);
user = await this.addContextOrWait(user);
user = this.cleanUserPropsForFetch(user);
this.debug('[Experiment] Fetch variants for user: ', user);
const results = await this.evaluationApi.getVariants(user, {
Expand Down Expand Up @@ -756,28 +758,25 @@ export class ExperimentClient implements Client {

private addContext(user: ExperimentUser): ExperimentUser {
const providedUser = this.userProvider?.getUser();
const integrationUser = this.integrationManager.getUser();
const mergedUserProperties = {
...user?.user_properties,
...providedUser?.user_properties,
...integrationUser.user_properties,
...user?.user_properties,
};
return {
library: `experiment-js-client/${PACKAGE_VERSION}`,
...this.userProvider?.getUser(),
...providedUser,
...integrationUser,
...user,
user_properties: mergedUserProperties,
};
}

private async addContextOrWait(
user: ExperimentUser,
ms: number,
): Promise<ExperimentUser> {
if (this.userProvider instanceof DefaultUserProvider) {
if (this.userProvider.userProvider instanceof ConnectorUserProvider) {
await this.userProvider.userProvider.identityReady(ms);
}
}

await this.integrationManager.ready();
return this.addContext(user);
}

Expand Down Expand Up @@ -823,6 +822,7 @@ export class ExperimentClient implements Client {
}
if (metadata) exposure.metadata = metadata;
this.exposureTrackingProvider?.track(exposure);
this.integrationManager.track(exposure);
}

private legacyExposureInternal(
Expand Down Expand Up @@ -855,6 +855,16 @@ export class ExperimentClient implements Client {
}
return true;
}

/**
* Add a plugin to the experiment client.
* @param plugin the plugin to add.
*/
public addPlugin(plugin: ExperimentPlugin): void {
if (plugin.type === 'integration') {
this.integrationManager.setIntegration(plugin as IntegrationPlugin);
}
}
}

type SourceVariant = {
Expand Down
56 changes: 18 additions & 38 deletions packages/experiment-browser/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { AnalyticsConnector } from '@amplitude/analytics-connector';

import { Defaults, ExperimentConfig } from './config';
import { ExperimentClient } from './experimentClient';
import {
ConnectorExposureTrackingProvider,
ConnectorUserProvider,
} from './integration/connector';
import { DefaultUserProvider } from './integration/default';
import { AmplitudeIntegrationPlugin } from './integration/amplitude';
import { DefaultUserProvider } from './providers/default';

const instances = {};

const getInstanceName = (config: ExperimentConfig): string => {
return config?.instanceName || Defaults.instanceName;
};

/**
* Initializes a singleton {@link ExperimentClient} identified by the configured
* instance name.
Expand All @@ -23,17 +24,12 @@ const initialize = (
): ExperimentClient => {
// Store instances by appending the instance name and api key. Allows for
// initializing multiple default instances for different api keys.
const instanceName = config?.instanceName || Defaults.instanceName;
const instanceName = getInstanceName(config);
const instanceKey = `${instanceName}.${apiKey}`;
const connector = AnalyticsConnector.getInstance(instanceName);
if (!instances[instanceKey]) {
config = {
...config,
userProvider: new DefaultUserProvider(
connector.applicationContextProvider,
config?.userProvider,
apiKey,
),
userProvider: new DefaultUserProvider(config?.userProvider, apiKey),
};
instances[instanceKey] = new ExperimentClient(apiKey, config);
}
Expand All @@ -55,32 +51,16 @@ const initializeWithAmplitudeAnalytics = (
apiKey: string,
config?: ExperimentConfig,
): ExperimentClient => {
// Store instances by appending the instance name and api key. Allows for
// initializing multiple default instances for different api keys.
const instanceName = config?.instanceName || Defaults.instanceName;
const instanceKey = `${instanceName}.${apiKey}`;
const connector = AnalyticsConnector.getInstance(instanceName);
if (!instances[instanceKey]) {
connector.eventBridge.setInstanceName(instanceName);
config = {
userProvider: new DefaultUserProvider(
connector.applicationContextProvider,
new ConnectorUserProvider(connector.identityStore),
apiKey,
),
exposureTrackingProvider: new ConnectorExposureTrackingProvider(
connector.eventBridge,
),
...config,
};
instances[instanceKey] = new ExperimentClient(apiKey, config);
if (config.automaticFetchOnAmplitudeIdentityChange) {
connector.identityStore.addIdentityListener(() => {
instances[instanceKey].fetch();
});
}
}
return instances[instanceKey];
const instanceName = getInstanceName(config);
const client = initialize(apiKey, config);
client.addPlugin(
new AmplitudeIntegrationPlugin(
apiKey,
AnalyticsConnector.getInstance(instanceName),
10000,
),
);
return client;
};

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/experiment-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export { ExperimentConfig } from './config';
export {
AmplitudeUserProvider,
AmplitudeAnalyticsProvider,
} from './integration/amplitude';
} from './providers/amplitude';
export { AmplitudeIntegrationPlugin } from './integration/amplitude';
export { Experiment } from './factory';
export { StubExperimentClient } from './stubClient';
export { ExperimentClient } from './experimentClient';
Expand All @@ -23,3 +24,9 @@ export { Source } from './types/source';
export { ExperimentUser } from './types/user';
export { Variant, Variants } from './types/variant';
export { Exposure, ExposureTrackingProvider } from './types/exposure';
export {
ExperimentPlugin,
IntegrationPlugin,
ExperimentPluginType,
ExperimentEvent,
} from './types/plugin';
Loading
Loading