Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "1.3.2-rc.1",
"version": "1.3.2-rc.4",
"description": "Split Javascript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
29 changes: 22 additions & 7 deletions src/listeners/__tests__/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { IEventsCacheSync, IImpressionCountsCacheSync, IImpressionsCacheSync, IS
import { ISplitApi } from '../../services/types';
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';

jest.mock('../../sync/submitters/telemetrySubmitter', () => {
return {
telemetryCacheStatsAdapter: () => {
return {
isEmpty: () => false,
clear: () => { },
state: () => ({}),
};
}
};
});

/* Mocks start */

const fakeImpression = {
Expand All @@ -26,6 +38,7 @@ const fakeImpressionCounts = {
'someFeature::0': 1
};

// Storage with impressionsCount and telemetry cache
const fakeStorageOptimized = { // @ts-expect-error
impressions: {
isEmpty: jest.fn(),
Expand All @@ -48,6 +61,7 @@ const fakeStorageOptimized = { // @ts-expect-error
return fakeImpressionCounts;
}
} as IImpressionCountsCacheSync,
telemetry: {}
};

const fakeStorageDebug = {
Expand All @@ -59,7 +73,8 @@ const fakeStorageDebug = {
const fakeSplitApi = {
postTestImpressionsBulk: jest.fn(() => Promise.resolve()),
postEventsBulk: jest.fn(() => Promise.resolve()),
postTestImpressionsCount: jest.fn(() => Promise.resolve())
postTestImpressionsCount: jest.fn(() => Promise.resolve()),
postMetricsUsage: jest.fn(() => Promise.resolve()),
} as ISplitApi;

const UNLOAD_DOM_EVENT = 'unload';
Expand Down Expand Up @@ -131,7 +146,7 @@ test('Browser JS listener / consumer mode', () => {
expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]);
});

test('Browser JS listener / standalone mode / Impressions optimized mode', () => {
test('Browser JS listener / standalone mode / Impressions optimized mode with telemetry', () => {
const syncManagerMock = {};

// @ts-expect-error
Expand All @@ -144,8 +159,8 @@ test('Browser JS listener / standalone mode / Impressions optimized mode', () =>

triggerUnloadEvent();

// Unload event was triggered. Thus sendBeacon method should have been called three times.
expect(global.window.navigator.sendBeacon).toBeCalledTimes(3);
// Unload event was triggered. Thus sendBeacon method should have been called four times.
expect(global.window.navigator.sendBeacon).toBeCalledTimes(4);

// Http post services should have not been called
expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled();
Expand Down Expand Up @@ -195,7 +210,7 @@ test('Browser JS listener / standalone mode / Impressions debug mode', () => {
});

test('Browser JS listener / standalone mode / Impressions debug mode without sendBeacon API', () => {
// remove sendBeacon API
// remove sendBeacon API temporally
const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error
global.navigator.sendBeacon = undefined;
const syncManagerMockWithoutPushManager = {};
Expand Down Expand Up @@ -254,8 +269,8 @@ test('Browser JS listener / standalone mode / user consent status', () => {
settings.userConsent = undefined;
triggerUnloadEvent();

// Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 6 times (3 times per event in optimized mode).
expect(global.window.navigator.sendBeacon).toBeCalledTimes(6);
// Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 8 times (4 times per event in optimized mode with telemetry).
expect(global.window.navigator.sendBeacon).toBeCalledTimes(8);

listener.stop();
});
7 changes: 6 additions & 1 deletion src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
import { ISyncManager } from '../sync/types';
import { isConsentGranted } from '../consent';
import { telemetryCacheStatsAdapter } from '../sync/submitters/telemetrySubmitter';

// 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring.
const UNLOAD_DOM_EVENT = 'unload';
Expand Down Expand Up @@ -77,7 +78,11 @@ export class BrowserSignalListener implements ISignalListener {
this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
// No beacon endpoint for `/metrics/usage`
if (this.storage.telemetry) {
const telemetryUrl = this.settings.urls.telemetry;
const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
}
}

// Close streaming connection
Expand Down
3 changes: 2 additions & 1 deletion src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ describe('SDK Readiness Manager - Event emitter', () => {

test('The event callbacks should work as expected - SDK_READY emits with expected internal callbacks', () => {
// the sdkReadinessManager expects more than one SDK_READY callback to not log the "No listeners" warning
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock, undefined /* default readyTimeout */, 1 /* internalReadyCbCount */);
const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock);
sdkReadinessManager.incInternalReadyCbCount();
const gateMock = sdkReadinessManager.readinessManager.gate;

// Get the callbacks
Expand Down
12 changes: 7 additions & 5 deletions src/readiness/sdkReadinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,16 @@ const REMOVE_LISTENER_EVENT = 'removeListener';
* It also updates logs related warnings and errors.
*
* @param readyTimeout time in millis to emit SDK_READY_TIME_OUT event
* @param internalReadyCbCount offset value of SDK_READY listeners that are added/removed internally
* by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
* @param readinessManager optional readinessManager to use. only used internally for `shared` method
*/
export function sdkReadinessManagerFactory(
log: ILogger,
EventEmitter: new () => IEventEmitter,
readyTimeout = 0,
internalReadyCbCount = 0,
readinessManager = readinessManagerFactory(EventEmitter, readyTimeout)): ISdkReadinessManager {

/** Ready callback warning */
let internalReadyCbCount = 0;
let readyCbCount = 0;
readinessManager.gate.on(REMOVE_LISTENER_EVENT, (event: any) => {
if (event === SDK_READY) readyCbCount--;
Expand Down Expand Up @@ -74,8 +72,12 @@ export function sdkReadinessManagerFactory(
return {
readinessManager,

shared(readyTimeout = 0, internalReadyCbCount = 0) {
return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, internalReadyCbCount, readinessManager.shared(readyTimeout));
shared(readyTimeout = 0) {
return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, readinessManager.shared(readyTimeout));
},

incInternalReadyCbCount() {
internalReadyCbCount++;
},

sdkStatus: objectAssign(
Expand Down
8 changes: 7 additions & 1 deletion src/readiness/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export interface ISdkReadinessManager {
readinessManager: IReadinessManager
sdkStatus: IStatusInterface

/**
* Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally
* by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
*/
incInternalReadyCbCount(): void

/** for client-side */
shared(readyTimeout?: number, internalReadyCbCount?: number): ISdkReadinessManager
shared(readyTimeout?: number): ISdkReadinessManager
}
4 changes: 2 additions & 2 deletions src/services/splitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ export function splitApiFactory(
},

postMetricsConfig(body: string) {
const url = `${urls.telemetry}/metrics/config`;
const url = `${urls.telemetry}/v1/metrics/config`;
return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true);
},

postMetricsUsage(body: string) {
const url = `${urls.telemetry}/metrics/usage`;
const url = `${urls.telemetry}/v1/metrics/usage`;
return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true);
}
};
Expand Down
5 changes: 2 additions & 3 deletions src/sync/submitters/__tests__/telemetrySubmitter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ describe('Telemetry submitter', () => {
settings: { ...fullSettings, scheduler: { ...fullSettings.scheduler, telemetryRefreshRate } },
splitApi: { postMetricsUsage, postMetricsConfig }, // @ts-ignore
storage: InMemoryStorageFactory({}),
platform: {
now: () => 123 // by returning a fixed timestamp, all latencies are equal to 0
},
platform: { now: () => 123 }, // by returning a fixed timestamp, all latencies are equal to 0
sdkReadinessManager: { incInternalReadyCbCount: jest.fn(), },
readiness: {
gate: {
once: jest.fn((e: string, cb: () => void) => {
Expand Down
8 changes: 4 additions & 4 deletions src/sync/submitters/submitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function submitterFactory<TState>(
dataName: string,
fromCacheToPayload?: (cacheData: TState) => any,
maxRetries: number = 0,
debugLogs?: boolean
debugLogs?: boolean // true for telemetry submitters
): ISyncTask<[], void> {

let retries = 0;
Expand All @@ -37,14 +37,14 @@ export function submitterFactory<TState>(
sourceCache.clear(); // we clear the queue if request successes.
}).catch(err => {
if (!maxRetries) {
log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
} else if (retries === maxRetries) {
retries = 0;
sourceCache.clear(); // we clear the queue if request fails after retries.
log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
} else {
retries++;
log.warn(SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]);
log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]);
}
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/sync/submitters/telemetrySubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) {
const { storage: { splits, segments, telemetry } } = params;
if (!telemetry) return; // No submitter created if telemetry cache is not defined

const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness } = params;
const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness, sdkReadinessManager } = params;
const startTime = timer(now || Date.now);

const submitter = firstPushWindowDecorator(
Expand All @@ -129,6 +129,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) {
telemetry.recordTimeUntilReadyFromCache(startTime());
});

sdkReadinessManager.incInternalReadyCbCount();
readiness.gate.once(SDK_READY, () => {
telemetry.recordTimeUntilReady(startTime());

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ export namespace SplitIO {
/**
* String property to override the base URL where the SDK will post telemetry data.
* @property {string} telemetry
* @default 'https://telemetry.split.io'
* @default 'https://telemetry.split.io/api'
*/
telemetry?: string
};
Expand Down
3 changes: 1 addition & 2 deletions src/utils/murmur3/utfx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
*/

export interface utfx {
encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void,

encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void
}


Expand Down
2 changes: 1 addition & 1 deletion src/utils/settingsValidation/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('settingsValidation', () => {
events: 'https://events.split.io/api',
auth: 'https://auth.split.io/api',
streaming: 'https://streaming.split.io',
telemetry: 'https://telemetry.split.io',
telemetry: 'https://telemetry.split.io/api',
});
expect(settings.sync.impressionsMode).toBe(OPTIMIZED);
});
Expand Down
2 changes: 1 addition & 1 deletion src/utils/settingsValidation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const base = {
// Streaming Server
streaming: 'https://streaming.split.io',
// Telemetry Server
telemetry: 'https://telemetry.split.io',
telemetry: 'https://telemetry.split.io/api',
},

// Defines which kind of storage we should instanciate.
Expand Down
2 changes: 1 addition & 1 deletion src/utils/settingsValidation/url.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ISettings } from '../../types';

const telemetryEndpointMatcher = /^\/metrics\/(config|usage)/;
const telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/;
const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/;
const authEndpointMatcher = /^\/v2\/auth/;
const streamingEndpointMatcher = /^\/(sse|event-stream)/;
Expand Down