Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Feature requests from Issues + Simple Client #29

Closed
wants to merge 3 commits into from
Closed
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
17 changes: 15 additions & 2 deletions packages/node/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Options, LogLevel } from '@amplitude/types';
import { NodeOptions, LogLevel, Response, Status } from '@amplitude/types';
export const SDK_NAME = 'amplitude-node';
export const SDK_VERSION = '0.3.3';
export const AMPLITUDE_SERVER_URL = 'https://api2.amplitude.com/2/httpapi';
export const BASE_RETRY_TIMEOUT = 100;
export const DEFAULT_OPTIONS: Options = {
export const DEFAULT_OPTIONS: NodeOptions = {
serverUrl: AMPLITUDE_SERVER_URL,
debug: false,
// 2kb is a safe estimate for a medium size event object. This keeps the SDK's memory footprint roughly
Expand All @@ -18,3 +18,16 @@ export const DEFAULT_OPTIONS: Options = {
// By default, events flush on the next event loop
uploadIntervalInSec: 0,
};

// A success response sent when the SDK didn't need to actually do anything
// But also successfully returned.
export const NOOP_SUCCESS_RESPONSE: Response = {
statusCode: 200,
status: Status.Success,
body: {
code: 200,
eventsIngested: 0,
payloadSizeBytes: 0,
serverUploadTime: 0,
},
};
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { Event, Options, Response, Status } from '@amplitude/types';
export { NodeClient } from './nodeClient';
export { SimpleClient } from './simpleClient';
export { RetryHandler } from './retryHandler';
export { init } from './sdk';
export { HTTPTransport } from './transports';
45 changes: 27 additions & 18 deletions packages/node/src/nodeClient.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { Client, Event, Options, Status, Response, RetryClass } from '@amplitude/types';
import { Client, Event, NodeOptions, Response, RetryClass } from '@amplitude/types';
import { logger } from '@amplitude/utils';
import { RetryHandler } from './retryHandler';
import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS } from './constants';
import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS, NOOP_SUCCESS_RESPONSE } from './constants';

export class NodeClient implements Client<Options> {
export class NodeClient implements Client<NodeOptions> {
/** Project Api Key */
protected readonly _apiKey: string;

/** Options for the client. */
protected readonly _options: Options;
protected readonly _options: NodeOptions;

private _events: Array<Event> = [];
private _transportWithRetry: RetryClass;
private _flushTimer: NodeJS.Timeout | null = null;
private _flushListeners: Array<(repsonse: Response) => void> = [];

/**
* Initializes this client instance.
*
* @param apiKey API key for your project
* @param options options for the client
*/
public constructor(apiKey: string, options: Partial<Options> = {}) {
public constructor(apiKey: string, options: Partial<NodeOptions> = {}) {
this._apiKey = apiKey;
this._options = Object.assign({}, DEFAULT_OPTIONS, options);
this._transportWithRetry = this._options.retryClass || this._setupDefaultTransport();
Expand All @@ -30,7 +31,7 @@ export class NodeClient implements Client<Options> {
/**
* @inheritDoc
*/
public getOptions(): Options {
public getOptions(): NodeOptions {
return this._options;
}

Expand All @@ -43,21 +44,26 @@ export class NodeClient implements Client<Options> {
clearTimeout(this._flushTimer);
}

let response = NOOP_SUCCESS_RESPONSE;
const flushListeners = this._flushListeners.splice(0, this._flushListeners.length);
// Check if there's 0 events, flush is not needed.
const arrayLength = this._events.length;
if (arrayLength === 0) {
return { status: Status.Success, statusCode: 200 };
if (arrayLength !== 0) {
const eventsToSend = this._events.splice(0, arrayLength);
response = await this._transportWithRetry.sendEventsWithRetry(eventsToSend);
}
const eventsToSend = this._events.splice(0, arrayLength);
return this._transportWithRetry.sendEventsWithRetry(eventsToSend);

flushListeners.forEach(listener => listener(response));

return response;
}

/**
* @inheritDoc
*/
public logEvent(event: Event): void {
public logEvent(event: Event): Promise<Response> {
if (this._options.optOut === true) {
return;
return Promise.resolve(NOOP_SUCCESS_RESPONSE);
}

this._annotateEvent(event);
Expand All @@ -66,14 +72,17 @@ export class NodeClient implements Client<Options> {

if (this._events.length >= this._options.maxCachedEvents) {
// # of events exceeds the limit, flush them.
this.flush();
return this.flush();
} else {
// Not ready to flush them and not timing yet, then set the timeout
if (this._flushTimer === null) {
this._flushTimer = setTimeout(() => {
this.flush();
}, this._options.uploadIntervalInSec * 1000);
}
return new Promise(resolve => {
if (this._flushTimer === null) {
this._flushListeners.push(resolve);
this._flushTimer = setTimeout(() => {
this.flush();
}, this._options.uploadIntervalInSec * 1000);
}
});
}
}

Expand Down
20 changes: 5 additions & 15 deletions packages/node/src/retryHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Event, Options, Transport, TransportOptions, Payload, Status, Response } from '@amplitude/types';
import { HTTPTransport } from './transports';
import { Event, NodeOptions, Transport, Payload, Status, Response } from '@amplitude/types';
import { setupTransportFromOptions } from './transports';
import { DEFAULT_OPTIONS, BASE_RETRY_TIMEOUT } from './constants';
import { asyncSleep } from '@amplitude/utils';

Expand All @@ -9,14 +9,14 @@ export class RetryHandler {
// A map of maps to event buffers for failed events
// The first key is userId (or ''), and second is deviceId (or '')
private _idToBuffer: Map<string, Map<string, Array<Event>>> = new Map<string, Map<string, Array<Event>>>();
private _options: Options;
private _options: NodeOptions;
private _transport: Transport;
private _eventsInRetry: number = 0;

public constructor(apiKey: string, options: Partial<Options>) {
public constructor(apiKey: string, options: Partial<NodeOptions>) {
this._apiKey = apiKey;
this._options = Object.assign({}, DEFAULT_OPTIONS, options);
this._transport = this._options.transportClass || this._setupDefaultTransport();
this._transport = this._options.transportClass || setupTransportFromOptions(this._options);
}

/**
Expand All @@ -39,16 +39,6 @@ export class RetryHandler {
}
}

private _setupDefaultTransport(): Transport {
const transportOptions: TransportOptions = {
serverUrl: this._options.serverUrl,
headers: {
'Content-Type': 'application/json',
},
};
return new HTTPTransport(transportOptions);
}

private _shouldRetryEvents(): boolean {
if (typeof this._options.maxRetries !== 'number' || this._options.maxRetries <= 0) {
return false;
Expand Down
89 changes: 89 additions & 0 deletions packages/node/src/simpleClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Client, Event, Options, Status, Response, Transport } from '@amplitude/types';
import { logger } from '@amplitude/utils';
import { setupTransportFromOptions } from './transports';
import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS, NOOP_SUCCESS_RESPONSE } from './constants';

export class SimpleClient implements Client<Options> {
/** Project Api Key */
protected readonly _apiKey: string;

/** Options for the client. */
protected readonly _options: Options;

protected readonly _transport: Transport;

/**
* Initializes this client instance.
*
* @param apiKey API key for your project
* @param options options for the client
*/
public constructor(apiKey: string, options: Partial<Options> = {}) {
this._apiKey = apiKey;
this._options = Object.assign({}, DEFAULT_OPTIONS, options);
this._transport = this._options.transportClass || setupTransportFromOptions(this._options);
this._setUpLogging();
}

/**
* @inheritDoc
*/
public getOptions(): Options {
return this._options;
}

/**
* @inheritDoc
*/
public async flush(): Promise<Response> {
// Noop
logger.warn('The simple client does nothing when flushing events.');
return {
statusCode: 200,
status: Status.Success,
body: {
code: 200,
eventsIngested: 0,
payloadSizeBytes: 0,
serverUploadTime: 0,
},
};
}

/**
* @inheritDoc
*/
public logEvent(event: Event): Promise<Response> {
return this.logEvents(event);
}

/**
*
* @param event
*/
public logEvents(...events: Array<Event>): Promise<Response> {
if (this._options.optOut === true) {
return Promise.resolve(NOOP_SUCCESS_RESPONSE);
}

events.forEach(event => this._annotateEvent(event));
// Immediately send the event
return this._transport.sendPayload({ events, api_key: this._apiKey });
}

/** Add platform dependent field onto event. */
private _annotateEvent(event: Event): void {
event.library = `${SDK_NAME}/${SDK_VERSION}`;
event.platform = 'Node.js';
}

private _setUpLogging(): void {
if (this._options.debug || this._options.logLevel) {
if (this._options.logLevel) {
logger.enable(this._options.logLevel);
} else {
logger.enable();
}
}
}
}
12 changes: 11 additions & 1 deletion packages/node/src/transports/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Payload, Response, Status, Transport, TransportOptions, mapJSONToResponse } from '@amplitude/types';
import { Options, Payload, Response, Status, Transport, TransportOptions, mapJSONToResponse } from '@amplitude/types';

import * as http from 'http';
import * as https from 'https';
Expand Down Expand Up @@ -164,3 +164,13 @@ export class HTTPTransport implements Transport {
});
}
}

export const setupTransportFromOptions = (options: Options): HTTPTransport => {
const transportOptions: TransportOptions = {
serverUrl: options.serverUrl,
headers: {
'Content-Type': 'application/json',
},
};
return new HTTPTransport(transportOptions);
};
2 changes: 1 addition & 1 deletion packages/node/src/transports/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { HTTPTransport } from './http';
export { HTTPTransport, setupTransportFromOptions } from './http';
3 changes: 2 additions & 1 deletion packages/types/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Event } from './event';
import { Options } from './options';
import { Response } from './response';

/**
* User-Facing Amplitude SDK Client.
Expand All @@ -17,7 +18,7 @@ export interface Client<O extends Options = Options> {
*
* @param event The event to send to Amplitude.
*/
logEvent(event: Event): void;
logEvent(event: Event): Promise<Response>;

/**
* Flush and send all the events which haven't been sent.
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { LogLevel } from './logger';
export { Client } from './client';
export { Event, Payload } from './event';
export { Options } from './options';
export { Options, NodeOptions } from './options';
export { Response, ResponseBody, mapJSONToResponse } from './response';
export { RetryClass } from './retry';
export { Status } from './status';
Expand Down
26 changes: 14 additions & 12 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,31 @@ export interface Options {
*/
logLevel: LogLevel;

/** The maximum events in the buffer */
maxCachedEvents: number;

/** The maximum number of times a server will attempt to retry */
maxRetries: number;

/**
* Whether you opt out from sending events.
*/
optOut: boolean;

/**
* The class being used to handle event retrying.
*/
retryClass: RetryClass | null;
/** If you're using a proxy server, set its url here. */
serverUrl: string;

/**
* The class being used to transport events.
*/
transportClass: Transport | null;
}

/** If you're using a proxy server, set its url here. */
serverUrl: string;
export interface NodeOptions extends Options {
/**
* The class being used to handle event retrying.
*/
retryClass: RetryClass | null;

/** The maximum events in the buffer */
maxCachedEvents: number;

/** The maximum number of times a server will attempt to retry */
maxRetries: number;

/** The events upload interval */
uploadIntervalInSec: number;
Expand Down