From 9caf38be7fb642e560079002af8850764f9b3a53 Mon Sep 17 00:00:00 2001 From: Kelvin Lu Date: Fri, 18 Sep 2020 02:39:25 -0700 Subject: [PATCH 1/3] add a simple client that does not flush or retry --- packages/node/src/constants.ts | 4 +- packages/node/src/nodeClient.ts | 10 ++-- packages/node/src/retryHandler.ts | 20 ++----- packages/node/src/simpleClient.ts | 81 +++++++++++++++++++++++++++ packages/node/src/transports/http.ts | 12 +++- packages/node/src/transports/index.ts | 2 +- packages/types/src/index.ts | 2 +- packages/types/src/options.ts | 26 +++++---- 8 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 packages/node/src/simpleClient.ts diff --git a/packages/node/src/constants.ts b/packages/node/src/constants.ts index e4c50d1..b97a508 100644 --- a/packages/node/src/constants.ts +++ b/packages/node/src/constants.ts @@ -1,9 +1,9 @@ -import { Options, LogLevel } from '@amplitude/types'; +import { NodeOptions, LogLevel } 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 diff --git a/packages/node/src/nodeClient.ts b/packages/node/src/nodeClient.ts index f3edbd4..9527fc2 100644 --- a/packages/node/src/nodeClient.ts +++ b/packages/node/src/nodeClient.ts @@ -1,14 +1,14 @@ -import { Client, Event, Options, Status, Response, RetryClass } from '@amplitude/types'; +import { Client, Event, NodeOptions, Status, Response, RetryClass } from '@amplitude/types'; import { logger } from '@amplitude/utils'; import { RetryHandler } from './retryHandler'; import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS } from './constants'; -export class NodeClient implements Client { +export class NodeClient implements Client { /** Project Api Key */ protected readonly _apiKey: string; /** Options for the client. */ - protected readonly _options: Options; + protected readonly _options: NodeOptions; private _events: Array = []; private _transportWithRetry: RetryClass; @@ -20,7 +20,7 @@ export class NodeClient implements Client { * @param apiKey API key for your project * @param options options for the client */ - public constructor(apiKey: string, options: Partial = {}) { + public constructor(apiKey: string, options: Partial = {}) { this._apiKey = apiKey; this._options = Object.assign({}, DEFAULT_OPTIONS, options); this._transportWithRetry = this._options.retryClass || this._setupDefaultTransport(); @@ -30,7 +30,7 @@ export class NodeClient implements Client { /** * @inheritDoc */ - public getOptions(): Options { + public getOptions(): NodeOptions { return this._options; } diff --git a/packages/node/src/retryHandler.ts b/packages/node/src/retryHandler.ts index 322d35e..fd75e39 100644 --- a/packages/node/src/retryHandler.ts +++ b/packages/node/src/retryHandler.ts @@ -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'; @@ -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>> = new Map>>(); - private _options: Options; + private _options: NodeOptions; private _transport: Transport; private _eventsInRetry: number = 0; - public constructor(apiKey: string, options: Partial) { + public constructor(apiKey: string, options: Partial) { 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); } /** @@ -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; diff --git a/packages/node/src/simpleClient.ts b/packages/node/src/simpleClient.ts new file mode 100644 index 0000000..5565a64 --- /dev/null +++ b/packages/node/src/simpleClient.ts @@ -0,0 +1,81 @@ +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 } from './constants'; + +export class NodeClient implements Client { + /** 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 = {}) { + 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 { + // 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): void { + if (this._options.optOut === true) { + return; + } + + this._annotateEvent(event); + // Immediately send the event + this._transport.sendPayload({ events: [event], 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(); + } + } + } +} diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index ed4e2df..ce0b9b5 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -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'; @@ -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); +}; diff --git a/packages/node/src/transports/index.ts b/packages/node/src/transports/index.ts index f61287c..da82ae1 100644 --- a/packages/node/src/transports/index.ts +++ b/packages/node/src/transports/index.ts @@ -1 +1 @@ -export { HTTPTransport } from './http'; +export { HTTPTransport, setupTransportFromOptions } from './http'; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fbd3553..df7665d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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'; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 919cc36..3917f49 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -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; From 4a7677bdbb02a27e6c43bc8c17840a221f5e9cb8 Mon Sep 17 00:00:00 2001 From: Kelvin Lu Date: Fri, 18 Sep 2020 02:54:01 -0700 Subject: [PATCH 2/3] have logevent return a promise --- packages/node/src/constants.ts | 15 ++++++++++++++- packages/node/src/index.ts | 1 + packages/node/src/nodeClient.ts | 28 ++++++++++++++++++---------- packages/node/src/simpleClient.ts | 2 +- packages/types/src/client.ts | 3 ++- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/node/src/constants.ts b/packages/node/src/constants.ts index b97a508..56215bd 100644 --- a/packages/node/src/constants.ts +++ b/packages/node/src/constants.ts @@ -1,4 +1,4 @@ -import { NodeOptions, 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'; @@ -18,3 +18,16 @@ export const DEFAULT_OPTIONS: NodeOptions = { // 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 UNSENT_SUCCESS_RESPONSE: Response = { + statusCode: 200, + status: Status.Success, + body: { + code: 200, + eventsIngested: 0, + payloadSizeBytes: 0, + serverUploadTime: 0, + }, +}; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index b942b80..e7937ae 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -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'; diff --git a/packages/node/src/nodeClient.ts b/packages/node/src/nodeClient.ts index 9527fc2..6243c20 100644 --- a/packages/node/src/nodeClient.ts +++ b/packages/node/src/nodeClient.ts @@ -1,7 +1,7 @@ import { Client, Event, NodeOptions, Status, 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, UNSENT_SUCCESS_RESPONSE } from './constants'; export class NodeClient implements Client { /** Project Api Key */ @@ -13,6 +13,7 @@ export class NodeClient implements Client { private _events: Array = []; private _transportWithRetry: RetryClass; private _flushTimer: NodeJS.Timeout | null = null; + private _flushListeners: Array<(repsonse: Response) => void> = []; /** * Initializes this client instance. @@ -49,15 +50,19 @@ export class NodeClient implements Client { return { status: Status.Success, statusCode: 200 }; } const eventsToSend = this._events.splice(0, arrayLength); - return this._transportWithRetry.sendEventsWithRetry(eventsToSend); + const flushListeners = this._flushListeners.splice(0, this._flushListeners.length); + const response = await this._transportWithRetry.sendEventsWithRetry(eventsToSend); + flushListeners.forEach(listener => listener(response)); + + return response; } /** * @inheritDoc */ - public logEvent(event: Event): void { + public logEvent(event: Event): Promise { if (this._options.optOut === true) { - return; + return Promise.resolve(UNSENT_SUCCESS_RESPONSE); } this._annotateEvent(event); @@ -66,14 +71,17 @@ export class NodeClient implements Client { 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); + } + }); } } diff --git a/packages/node/src/simpleClient.ts b/packages/node/src/simpleClient.ts index 5565a64..c51ccb0 100644 --- a/packages/node/src/simpleClient.ts +++ b/packages/node/src/simpleClient.ts @@ -3,7 +3,7 @@ import { logger } from '@amplitude/utils'; import { setupTransportFromOptions } from './transports'; import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS } from './constants'; -export class NodeClient implements Client { +export class SimpleClient implements Client { /** Project Api Key */ protected readonly _apiKey: string; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index aa1a051..ffc98db 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,5 +1,6 @@ import { Event } from './event'; import { Options } from './options'; +import { Response } from './response'; /** * User-Facing Amplitude SDK Client. @@ -17,7 +18,7 @@ export interface Client { * * @param event The event to send to Amplitude. */ - logEvent(event: Event): void; + logEvent(event: Event): Promise; /** * Flush and send all the events which haven't been sent. From d04b6f4bea0e011bd66a23b0e253d843b75d9909 Mon Sep 17 00:00:00 2001 From: Kelvin Lu Date: Fri, 18 Sep 2020 02:59:26 -0700 Subject: [PATCH 3/3] logevents, fix type errors --- packages/node/src/constants.ts | 2 +- packages/node/src/nodeClient.ts | 17 +++++++++-------- packages/node/src/simpleClient.ts | 18 +++++++++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/node/src/constants.ts b/packages/node/src/constants.ts index 56215bd..c092947 100644 --- a/packages/node/src/constants.ts +++ b/packages/node/src/constants.ts @@ -21,7 +21,7 @@ export const DEFAULT_OPTIONS: NodeOptions = { // A success response sent when the SDK didn't need to actually do anything // But also successfully returned. -export const UNSENT_SUCCESS_RESPONSE: Response = { +export const NOOP_SUCCESS_RESPONSE: Response = { statusCode: 200, status: Status.Success, body: { diff --git a/packages/node/src/nodeClient.ts b/packages/node/src/nodeClient.ts index 6243c20..1649446 100644 --- a/packages/node/src/nodeClient.ts +++ b/packages/node/src/nodeClient.ts @@ -1,7 +1,7 @@ -import { Client, Event, NodeOptions, 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, UNSENT_SUCCESS_RESPONSE } from './constants'; +import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS, NOOP_SUCCESS_RESPONSE } from './constants'; export class NodeClient implements Client { /** Project Api Key */ @@ -44,14 +44,15 @@ export class NodeClient implements Client { 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); - const flushListeners = this._flushListeners.splice(0, this._flushListeners.length); - const response = await this._transportWithRetry.sendEventsWithRetry(eventsToSend); + flushListeners.forEach(listener => listener(response)); return response; @@ -62,7 +63,7 @@ export class NodeClient implements Client { */ public logEvent(event: Event): Promise { if (this._options.optOut === true) { - return Promise.resolve(UNSENT_SUCCESS_RESPONSE); + return Promise.resolve(NOOP_SUCCESS_RESPONSE); } this._annotateEvent(event); diff --git a/packages/node/src/simpleClient.ts b/packages/node/src/simpleClient.ts index c51ccb0..7af4c12 100644 --- a/packages/node/src/simpleClient.ts +++ b/packages/node/src/simpleClient.ts @@ -1,7 +1,7 @@ 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 } from './constants'; +import { SDK_NAME, SDK_VERSION, DEFAULT_OPTIONS, NOOP_SUCCESS_RESPONSE } from './constants'; export class SimpleClient implements Client { /** Project Api Key */ @@ -53,14 +53,22 @@ export class SimpleClient implements Client { /** * @inheritDoc */ - public logEvent(event: Event): void { + public logEvent(event: Event): Promise { + return this.logEvents(event); + } + + /** + * + * @param event + */ + public logEvents(...events: Array): Promise { if (this._options.optOut === true) { - return; + return Promise.resolve(NOOP_SUCCESS_RESPONSE); } - this._annotateEvent(event); + events.forEach(event => this._annotateEvent(event)); // Immediately send the event - this._transport.sendPayload({ events: [event], api_key: this._apiKey }); + return this._transport.sendPayload({ events, api_key: this._apiKey }); } /** Add platform dependent field onto event. */