Skip to content

Commit

Permalink
feat: add support for different transport types
Browse files Browse the repository at this point in the history
  • Loading branch information
saikumarrs committed Aug 16, 2024
1 parent fbb0616 commit da6a742
Show file tree
Hide file tree
Showing 49 changed files with 1,613 additions and 538 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
- name: Execute code quality checks
run: |
npm run check:circular
npm run check:duplicates
- name: Build the project
Expand Down
4 changes: 4 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"inputs": ["default", "^production"],
"cache": true
},
"build:browser:dev": {
"inputs": ["default", "^production"],
"cache": true
},
"check:size:json": {
"inputs": [
"default",
Expand Down
1,103 changes: 1,025 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"@segment/top-domain": "3.0.1",
"@vespaiach/axios-fetch-adapter": "0.3.1",
"assert": "2.1.0",
"axios": "1.7.3",
"axios": "1.7.4",
"axios-retry": "4.5.0",
"component-each": "0.2.6",
"component-emitter": "2.0.0",
Expand Down Expand Up @@ -135,10 +135,10 @@
"@size-limit/file": "11.1.4",
"@size-limit/webpack": "11.1.4",
"@swc-node/register": "1.10.9",
"@swc/core": "1.7.6",
"@swc/core": "1.7.11",
"@types/component-emitter": "1.2.14",
"@types/jest": "29.5.12",
"@types/node": "22.1.0",
"@types/node": "22.3.0",
"@types/ramda": "0.30.1",
"babel-plugin-transform-object-hasown": "1.1.0",
"commitizen": "4.3.0",
Expand Down Expand Up @@ -169,7 +169,8 @@
"jest-watch-typeahead": "2.2.2",
"join-component": "1.1.0",
"jscpd": "4.0.5",
"lint-staged": "15.2.8",
"lint-staged": "15.2.9",
"madge": "8.0.0",
"msw": "2.3.5",
"nx": "19.5.7",
"patch-package": "8.0.0",
Expand All @@ -180,7 +181,7 @@
"rollup-plugin-delete": "2.0.0",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-exclude-dependencies-from-bundle": "1.1.23",
"rollup-plugin-external-globals": "0.11.0",
"rollup-plugin-external-globals": "0.12.0",
"rollup-plugin-filesize": "10.0.0",
"rollup-plugin-generate-html-template": "1.7.0",
"rollup-plugin-livereload": "2.0.5",
Expand Down
8 changes: 0 additions & 8 deletions packages/analytics-js-common/__fixtures__/msw.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ const handlers = [
},
});
}),
http.get(`${dummyDataplaneHost}/jsonSample`, () => {
return new HttpResponse(JSON.stringify({ json: 'sample' }), {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
}),
http.get(`${dummyDataplaneHost}/404ErrorSample`, () => {
return new HttpResponse(null, {
status: 404,
Expand Down
74 changes: 49 additions & 25 deletions packages/analytics-js-common/src/types/HttpClient.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
import type { IErrorHandler } from './ErrorHandler';
import type { ILogger } from './Logger';

export type XHRResponseDetails = {
response: string;
error?: Error;
export interface IHttpClientError extends Error {
status?: number;
statusText?: string;
responseBody?: string | null;
}

export interface IResponseDetails {
response?: Response;
error?: IHttpClientError;
url: string | URL;
xhr?: XMLHttpRequest;
options: IXHRRequestOptions;
};
options: IRequestOptions;
}

export type AsyncRequestCallback<T> = (
data?: T | string | undefined,
details?: XHRResponseDetails,
data: T | string | undefined | null,
details: IResponseDetails,
) => void;

export interface IAsyncRequestConfig<T> {
url: string | URL;
options?: IXHRRequestOptions | IFetchRequestOptions | IBeaconRequestOptions;
options?: IRequestOptions;
isRawResponse?: boolean;
timeout?: number;
callback?: AsyncRequestCallback<T>;
}

export interface IRequestOptions {
method: HTTPClientMethod;
headers?: Record<string, string | undefined>;
export interface IBaseRequestOptions {
sendRawData?: boolean;
withCredentials?: boolean;
}

export interface IXHRRequestOptions extends IRequestOptions {
data?: Document | XMLHttpRequestBodyInit | null;
}

export interface IFetchRequestOptions extends IRequestOptions {
data?: BodyInit | null;
keepalive?: boolean;
}

export interface IBeaconRequestOptions extends IRequestOptions {
data?: BodyInit | null;
}
export type IRequestOptions = IXHRRequestOptions | IFetchRequestOptions | IBeaconRequestOptions;

export type HTTPClientMethod =
| 'GET'
Expand All @@ -57,7 +48,40 @@ export interface IHttpClient {
errorHandler?: IErrorHandler;
logger?: ILogger;
basicAuthHeader?: string;
transportFn: (url: string | URL, options: any) => Promise<Response>;
getAsyncData<T = any>(config: IAsyncRequestConfig<T>): void;
request<T = any>(config: IAsyncRequestConfig<T>): void;
setAuthHeader(value: string, noBto?: boolean): void;
resetAuthHeader(): void;
}

export interface IXHRRequestOptions
extends Omit<
RequestInit,
| 'body'
| 'mode'
| 'cache'
| 'redirect'
| 'referrerPolicy'
| 'integrity'
| 'keepalive'
| 'method'
>,
IBaseRequestOptions {
withCredentials?: boolean;
body?: Document | XMLHttpRequestBodyInit | null;
timeout?: number; // timeout in milliseconds
method: HTTPClientMethod;
}

export interface IFetchRequestOptions
extends Omit<RequestInit, 'body' | 'method'>,
IBaseRequestOptions {
body?: BodyInit | null;
timeout?: number; // timeout in milliseconds
method: HTTPClientMethod;
}

export interface IBeaconRequestOptions extends IBaseRequestOptions {
body?: BodyInit | null;
}
7 changes: 4 additions & 3 deletions packages/analytics-js-common/src/types/LoadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type BeaconQueueOpts = {
flushQueueInterval?: number;
};

export type EventsTransportMode = 'xhr' | 'beacon';
export type TransportType = 'fetch' | 'xhr' | 'beacon';

export type BatchOpts = {
// Whether to enable batching
Expand Down Expand Up @@ -143,8 +143,9 @@ export type LoadOptions = {
dataPlaneEventsBufferTimeout?: number;
storage?: StorageOpts;
preConsent?: PreConsentOptions;
// transport mechanism to be used for sending batched requests
transportMode?: EventsTransportMode; // Unused for now. This will deprecate the useBeacon and beaconQueueOptions
// transport mechanism to be used for sending events
// This will deprecate the useBeacon and beaconQueueOptions
deliveryMethod?: TransportType;
consentManagement?: ConsentManagementOptions;
sameDomainCookiesOnly?: boolean;
externalAnonymousIdCookieName?: string;
Expand Down
10 changes: 5 additions & 5 deletions packages/analytics-js-common/src/utilities/http.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { XHRResponseDetails } from '../types/HttpClient';
import type { IResponseDetails } from '../types/HttpClient';

const isErrRetryable = (details?: XHRResponseDetails) => {
const isErrRetryable = (details: IResponseDetails) => {
let isRetryableNWFailure = false;
if (details?.error && details?.xhr) {
const xhrStatus = details.xhr.status;
if (details.error?.status) {
const { status } = details.error;
// same as in v1.1
isRetryableNWFailure = xhrStatus === 429 || (xhrStatus >= 500 && xhrStatus < 600);
isRetryableNWFailure = status === 429 || (status >= 500 && status < 600);
}
return isRetryableNWFailure;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe('Plugin - CustomConsentManager', () => {
),
).toBe(true);

expect(mockErrorHandler.onError).toBeCalledWith(
expect(mockErrorHandler.onError).toHaveBeenCalledWith(
new TypeError("Cannot read properties of null (reading 'includes')"),
'CustomConsentManagerPlugin',
'Failed to determine the consent status for the destination. Please check the destination configuration and try again.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { defaultErrorHandler } from '@rudderstack/analytics-js/services/ErrorHan
import { defaultLogger } from '@rudderstack/analytics-js/services/Logger';
import { StoreManager } from '@rudderstack/analytics-js/services/StoreManager';
import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event';
import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient';
import type {
IHttpClient,
IResponseDetails,
} from '@rudderstack/analytics-js-common/types/HttpClient';
import {
dummyDataplaneHost,
dummyWriteKey,
Expand All @@ -18,6 +21,7 @@ import {
import { server } from '../../__fixtures__/msw.server';
import * as utils from '@rudderstack/analytics-js-plugins/deviceModeTransformation/utilities';
import { DeviceModeTransformation } from '@rudderstack/analytics-js-plugins/deviceModeTransformation';
import { HttpClientError } from '@rudderstack/analytics-js/services/HttpClient/utils';

jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({
...jest.requireActual('@rudderstack/analytics-js-common/utilities/uuId'),
Expand All @@ -42,7 +46,7 @@ describe('Device mode transformation plugin', () => {
});
});

const httpClient = new HttpClient();
const httpClient = new HttpClient('fetch');

afterAll(() => {
server.close();
Expand Down Expand Up @@ -121,7 +125,7 @@ describe('Device mode transformation plugin', () => {

DeviceModeTransformation().transformEvent?.enqueue(state, queue, event, destinations);

expect(addItemSpy).toBeCalledWith({
expect(addItemSpy).toHaveBeenCalledWith({
token: authToken,
destinationIds,
event,
Expand All @@ -132,7 +136,7 @@ describe('Device mode transformation plugin', () => {

it('should process queue item on start', () => {
const mockHttpClient = {
getAsyncData: ({ callback }) => {
request: ({ callback }) => {
callback(true);
},
setAuthHeader: jest.fn(),
Expand Down Expand Up @@ -164,7 +168,7 @@ describe('Device mode transformation plugin', () => {
// In actual implementation, this is done based on the state signals
queue.start();

expect(queueProcessCbSpy).toBeCalledWith(
expect(queueProcessCbSpy).toHaveBeenCalledWith(
{
token: authToken,
destinationIds,
Expand All @@ -184,8 +188,10 @@ describe('Device mode transformation plugin', () => {

it('SendTransformedEventToDestinations function is called in case of successful transformation', () => {
const mockHttpClient: IHttpClient = {
getAsyncData: ({ callback }) => {
callback?.(JSON.stringify(dmtSuccessResponse), { xhr: { status: 200 } });
request: ({ callback }) => {
callback?.(JSON.stringify(dmtSuccessResponse), {
response: { status: 200 } as Response,
} as IResponseDetails);
},
setAuthHeader: jest.fn(),
};
Expand Down Expand Up @@ -218,7 +224,7 @@ describe('Device mode transformation plugin', () => {
queue.start();
DeviceModeTransformation().transformEvent?.enqueue(state, queue, event, destinations);

expect(mockSendTransformedEventToDestinations).toBeCalledTimes(1);
expect(mockSendTransformedEventToDestinations).toHaveBeenCalledTimes(1);
expect(mockSendTransformedEventToDestinations).toHaveBeenCalledWith(
state,
defaultPluginsManager,
Expand All @@ -233,8 +239,8 @@ describe('Device mode transformation plugin', () => {
});
it('SendTransformedEventToDestinations function should not be called in case of unsuccessful transformation', () => {
const mockHttpClient: IHttpClient = {
getAsyncData: ({ callback }) => {
callback?.(false, { error: 'some error', xhr: { status: 502 } });
request: ({ callback }) => {
callback?.(null, { error: new HttpClientError('some error', 502) } as IResponseDetails);
},
setAuthHeader: jest.fn(),
};
Expand Down Expand Up @@ -267,7 +273,7 @@ describe('Device mode transformation plugin', () => {
queue.start();
DeviceModeTransformation().transformEvent?.enqueue(state, queue, event, destinations);

expect(mockSendTransformedEventToDestinations).not.toBeCalled();
expect(mockSendTransformedEventToDestinations).not.toHaveBeenCalled();
// The element is requeued
expect(queue.getStorageEntry('queue')).toStrictEqual([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('Plugin - ErrorReporting', () => {
metricsServiceUrl: signal('https://test.com'),
};
const mockHttpClient = {
getAsyncData: jest.fn(),
request: jest.fn(),
setAuthHeader: jest.fn(),
} as unknown as IHttpClient;
const newError = new Error();
Expand All @@ -133,7 +133,7 @@ describe('Plugin - ErrorReporting', () => {
},
);

expect(mockHttpClient.getAsyncData).not.toHaveBeenCalled();
expect(mockHttpClient.request).not.toHaveBeenCalled();
});

it('should send data to metrics service on notify when httpClient is provided', () => {
Expand All @@ -144,7 +144,7 @@ describe('Plugin - ErrorReporting', () => {
metricsServiceUrl: signal('https://test.com'),
};
const mockHttpClient = {
getAsyncData: jest.fn(),
request: jest.fn(),
setAuthHeader: jest.fn(),
} as unknown as IHttpClient;
const newError = new Error();
Expand All @@ -168,12 +168,12 @@ describe('Plugin - ErrorReporting', () => {
},
);

expect(mockHttpClient.getAsyncData).toHaveBeenCalled();
expect(mockHttpClient.request).toHaveBeenCalled();
});

it('should not notify if the error is not an SDK error', () => {
const mockHttpClient = {
getAsyncData: jest.fn(),
request: jest.fn(),
setAuthHeader: jest.fn(),
} as unknown as IHttpClient;
const newError = new Error();
Expand All @@ -197,7 +197,7 @@ describe('Plugin - ErrorReporting', () => {
},
);

expect(mockHttpClient.getAsyncData).not.toHaveBeenCalled();
expect(mockHttpClient.request).not.toHaveBeenCalled();
});

it('should add a new breadcrumb', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ describe('Error Reporting utilities', () => {
};
(window as any).RudderSnippetVersion = 'sample_snippet_version';
const enhancedError = getBugsnagErrorEvent(errorPayload, errorState, appState);
console.log(JSON.stringify(enhancedError));
const expectedOutcome = {
notifier: {
name: 'RudderStack JavaScript SDK Error Notifier',
Expand Down
Loading

0 comments on commit da6a742

Please sign in to comment.