diff --git a/README.md b/README.md index 05e021b..be4a960 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,16 @@ const sdk = new absmartly.SDK({ }); ``` +The `application` option can also be an object with `name` and `version` to track which version of your application is generating events. The version can be a number or a semver string: +```javascript +const sdk = new absmartly.SDK({ + endpoint: 'https://sandbox.absmartly.io/v1', + apiKey: process.env.ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: { name: 'website', version: '1.2.3' }, +}); +``` + #### Creating a new Context with raw promises ```javascript // define a new context request @@ -137,6 +147,28 @@ context.attributes({ }); ``` +#### Including system attributes +You can opt in to automatically include system attributes (SDK name, SDK version, application, environment, and application version) in every publish payload. These are sent as context attributes and can be useful for debugging and filtering in the Web Console. + +To enable this, set the `includeSystemAttributes` option to `true` when creating the context: +```javascript +const context = sdk.createContext(request, { + includeSystemAttributes: true, +}); +``` + +When enabled, the following attributes are automatically prepended to the publish request payload: + +| Attribute | Description | +|:--- |---| +| `sdk_name` | The SDK agent name (e.g. `"absmartly-javascript-sdk"`) | +| `sdk_version` | The SDK version (e.g. `"1.13.4"`) | +| `application` | The application name from the SDK configuration | +| `environment` | The environment from the SDK configuration | +| `app_version` | The application version, only included if greater than `0` | + +These system attributes are prepended before any user-defined attributes. + #### Selecting a treatment ```javascript if (context.treament("exp_test_experiment") == 0) { diff --git a/package.json b/package.json index 07ea94e..380e9ad 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,13 @@ "build-browser": "TARGET=browser webpack --progress --config webpack.config.js && TARGET=browser NODE_ENV=production webpack --progress --config webpack.config.js", "build-cjs": "TARGET=cjs babel js --delete-dir-on-start --ignore 'browser.js' -d lib", "build-es": "TARGET=es babel js --delete-dir-on-start --ignore 'browser.js' -d es", - "build": "npm run -s format && npm run -s lint && npm run -s compile && npm run -s test && npm run -s build-es && npm run -s build-cjs && npm run -s build-browser", + "build": "npm run -s format && npm run -s lint && npm run -s generate-version && npm run -s compile && npm run -s test && npm run -s build-es && npm run -s build-cjs && npm run -s build-browser", "lint": "eslint -f stylish 'src/**/*.{js,mjs,jsx,ts,mts,tsx}'", "format": "prettier --write '**/*.{js,mjs,jsx,json,ts,mts,tsx}'", "test": "jest --coverage", "prepack": "npm run -s build", - "compile": "tsc" + "compile": "tsc", + "generate-version": "node scripts/generate-version.js" }, "dependencies": { "node-fetch": "^2.6.7", diff --git a/scripts/generate-version.js b/scripts/generate-version.js new file mode 100644 index 0000000..8824f3c --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,7 @@ +const fs = require("fs"); +const path = require("path"); + +const pkg = require(path.resolve(__dirname, "../package.json")); +const versionFile = path.resolve(__dirname, "../src/version.ts"); + +fs.writeFileSync(versionFile, `export const SDK_VERSION = "${pkg.version}";\n`); diff --git a/src/__tests__/client.test.js b/src/__tests__/client.test.js index 5d0d3b6..7834ff4 100644 --- a/src/__tests__/client.test.js +++ b/src/__tests__/client.test.js @@ -4,6 +4,7 @@ import fetch from "../fetch"; // eslint-disable-next-line no-shadow import { AbortController } from "../abort"; import { AbortError, RetryError, TimeoutError } from "../errors"; //eslint-disable-line no-shadow +import { SDK_VERSION } from "../version"; jest.mock("../fetch"); @@ -830,6 +831,7 @@ describe("Client", () => { .publish({ units, publishedAt, + sdkVersion: SDK_VERSION, goals, exposures, attributes, @@ -850,6 +852,7 @@ describe("Client", () => { body: JSON.stringify({ units, publishedAt, + sdkVersion: SDK_VERSION, goals, exposures, attributes, @@ -872,6 +875,7 @@ describe("Client", () => { .publish({ units, publishedAt, + sdkVersion: SDK_VERSION, goals: [], exposures: [], }) @@ -891,6 +895,7 @@ describe("Client", () => { body: JSON.stringify({ units, publishedAt, + sdkVersion: SDK_VERSION, }), signal: expect.any(Object), }); @@ -910,6 +915,7 @@ describe("Client", () => { client .publish({ units, + sdkVersion: SDK_VERSION, goals: [], exposures: [], }) @@ -929,6 +935,7 @@ describe("Client", () => { body: JSON.stringify({ units, publishedAt: publishedAt + 100, + sdkVersion: SDK_VERSION, }), signal: expect.any(Object), }); @@ -972,6 +979,58 @@ describe("Client", () => { }); }); + it("publish() should include sdkVersion in body", (done) => { + fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); + + const client = new Client(clientOptions); + + client + .publish({ + units, + publishedAt, + sdkVersion: "1.2.3", + goals: [], + exposures: [], + }) + .then(() => { + const body = JSON.parse(fetch.mock.calls[0][1].body); + expect(body.sdkVersion).toEqual("1.2.3"); + + done(); + }); + }); + + it("getAgent() should return default agent when not specified", () => { + const { agent: _, ...optionsWithoutAgent } = clientOptions; + const client = new Client(optionsWithoutAgent); + expect(client.getAgent()).toEqual("javascript-client"); + }); + + it("getAgent() should return custom agent when specified", () => { + const client = new Client({ ...clientOptions, agent: "custom-sdk" }); + expect(client.getAgent()).toEqual("custom-sdk"); + }); + + it("getApplication() should return normalized application object", () => { + const client = new Client(clientOptions); + expect(client.getApplication()).toEqual({ name: "test_app", version: 1000000 }); + }); + + it("getApplication() should normalize string application to object", () => { + const client = new Client({ ...clientOptions, application: "website" }); + expect(client.getApplication()).toEqual({ name: "website", version: 0 }); + }); + + it("getApplication() should accept semver string version", () => { + const client = new Client({ ...clientOptions, application: { name: "website", version: "1.2.3" } }); + expect(client.getApplication()).toEqual({ name: "website", version: "1.2.3" }); + }); + + it("getEnvironment() should return the environment", () => { + const client = new Client(clientOptions); + expect(client.getEnvironment()).toEqual(environment); + }); + it("publish() should not have the keepalive flag if specified", (done) => { fetch.mockResolvedValueOnce(responseMock(200, "OK", defaultMockResponse)); diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index 0dc3f89..bac768c 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -5,6 +5,7 @@ import { hashUnit } from "../utils"; import clone from "rfdc/default"; import { ContextPublisher } from "../publisher"; import { ContextDataProvider } from "../provider"; +import { SDK_VERSION } from "../version"; jest.mock("../client"); jest.mock("../sdk"); @@ -473,6 +474,10 @@ describe("Context", () => { sdk.getClient.mockReturnValue(client); sdk.getEventLogger.mockReturnValue(SDK.defaultEventLogger); + client.getAgent.mockReturnValue("absmartly-javascript-sdk"); + client.getApplication.mockReturnValue({ name: "website", version: 0 }); + client.getEnvironment.mockReturnValue("production"); + const contextOptions = { publishDelay: -1, refreshPeriod: 0, @@ -745,6 +750,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -858,6 +864,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -1390,6 +1397,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -1552,6 +1560,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 0, @@ -1593,6 +1602,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, attributes: [ { name: "age", @@ -1640,6 +1650,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -1680,6 +1691,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -1741,6 +1753,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -2092,6 +2105,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -2315,6 +2329,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, attributes: [ { name: "age", @@ -2362,6 +2377,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -2402,6 +2418,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -2596,6 +2613,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, goals: [ { achievedAt: 1611141535729, @@ -2754,6 +2772,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, goals: [ { name: "goal1", @@ -2839,6 +2858,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -2960,6 +2980,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, goals: [ { name: "goal1", @@ -3013,6 +3034,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, goals: [ { achievedAt: 1611141535729, @@ -3073,6 +3095,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -3150,6 +3173,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, goals: [ { name: "goal2", @@ -3332,6 +3356,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -3382,6 +3407,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -3581,6 +3607,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -3642,6 +3669,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 2, @@ -3689,6 +3717,7 @@ describe("Context", () => { publishedAt: 1611141535729, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 3, @@ -3758,6 +3787,7 @@ describe("Context", () => { publishedAt: 1611141535829, units: publishUnits, hashed: true, + sdkVersion: SDK_VERSION, exposures: [ { id: 1, @@ -3922,4 +3952,227 @@ describe("Context", () => { expect(value).toEqual("string"); }); }); + + describe("includeSystemAttributes", () => { + it("should not include system attributes by default", (done) => { + const defaultOptions = { + publishDelay: -1, + refreshPeriod: 0, + }; + + const context = new Context(sdk, defaultOptions, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + expect(request.attributes).toBeUndefined(); + + done(); + }); + }); + + it("should include system attributes when includeSystemAttributes is true", (done) => { + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + expect(request.attributes).toBeDefined(); + expect(request.attributes.length).toBeGreaterThanOrEqual(4); + + const sdkNameAttr = request.attributes.find((a) => a.name === "sdk_name"); + const sdkVersionAttr = request.attributes.find((a) => a.name === "sdk_version"); + const applicationAttr = request.attributes.find((a) => a.name === "application"); + const environmentAttr = request.attributes.find((a) => a.name === "environment"); + + expect(sdkNameAttr).toBeDefined(); + expect(sdkNameAttr.value).toEqual("absmartly-javascript-sdk"); + expect(sdkNameAttr.setAt).toEqual(expect.any(Number)); + + expect(sdkVersionAttr).toBeDefined(); + expect(sdkVersionAttr.value).toEqual(SDK_VERSION); + expect(sdkVersionAttr.setAt).toEqual(expect.any(Number)); + + expect(applicationAttr).toBeDefined(); + expect(applicationAttr.value).toEqual("website"); + expect(applicationAttr.setAt).toEqual(expect.any(Number)); + + expect(environmentAttr).toBeDefined(); + expect(environmentAttr.value).toEqual("production"); + expect(environmentAttr.setAt).toEqual(expect.any(Number)); + + done(); + }); + }); + + it("should prepend system attributes before user attributes", (done) => { + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.attribute("custom_attr", "custom_value"); + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + expect(request.attributes[0].name).toEqual("sdk_name"); + expect(request.attributes[1].name).toEqual("sdk_version"); + expect(request.attributes[2].name).toEqual("application"); + expect(request.attributes[3].name).toEqual("environment"); + expect(request.attributes[4].name).toEqual("custom_attr"); + expect(request.attributes[4].value).toEqual("custom_value"); + + done(); + }); + }); + + it("should include app_version when application version is set", (done) => { + client.getApplication.mockReturnValueOnce({ name: "website", version: 3 }); + + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + const appVersionAttr = request.attributes.find((a) => a.name === "app_version"); + expect(appVersionAttr).toBeDefined(); + expect(appVersionAttr.value).toEqual(3); + + done(); + }); + }); + + it("should not include app_version when application version is 0", (done) => { + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + const appVersionAttr = request.attributes.find((a) => a.name === "app_version"); + expect(appVersionAttr).toBeUndefined(); + + done(); + }); + }); + + it("should include app_version when application version is a semver string", (done) => { + client.getApplication.mockReturnValueOnce({ name: "website", version: "1.2.3" }); + + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + const appVersionAttr = request.attributes.find((a) => a.name === "app_version"); + expect(appVersionAttr).toBeDefined(); + expect(appVersionAttr.value).toEqual("1.2.3"); + + done(); + }); + }); + + it("should include app_version with application as plain string", (done) => { + client.getApplication.mockReturnValueOnce({ name: "website", version: 0 }); + + const optionsWithSystemAttrs = { + publishDelay: -1, + refreshPeriod: 0, + includeSystemAttributes: true, + }; + + const context = new Context(sdk, optionsWithSystemAttrs, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + const applicationAttr = request.attributes.find((a) => a.name === "application"); + expect(applicationAttr).toBeDefined(); + expect(applicationAttr.value).toEqual("website"); + + const appVersionAttr = request.attributes.find((a) => a.name === "app_version"); + expect(appVersionAttr).toBeUndefined(); + + done(); + }); + }); + + it("should only include user attributes when includeSystemAttributes is not set", (done) => { + const defaultOptions = { + publishDelay: -1, + refreshPeriod: 0, + }; + + const context = new Context(sdk, defaultOptions, contextParams, getContextResponse); + publisher.publish.mockReturnValue(Promise.resolve()); + + context.attribute("custom_attr", "custom_value"); + context.treatment("exp_test_ab"); + + context.publish().then(() => { + const call = publisher.publish.mock.calls[0]; + const request = call[0]; + + expect(request.attributes).toEqual([ + { name: "custom_attr", value: "custom_value", setAt: expect.any(Number) }, + ]); + + done(); + }); + }); + }); }); diff --git a/src/client.ts b/src/client.ts index 6afdf66..3f934bb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,7 +4,6 @@ import { AbortController } from "./abort"; // eslint-disable-next-line no-shadow import { AbortError, RetryError, TimeoutError } from "./errors"; -import { getApplicationName, getApplicationVersion } from "./utils"; import { AbortSignal as ABsmartlyAbortSignal } from "./abort-controller-shim"; import { ContextOptions, ContextParams } from "./context"; import { PublishParams } from "./publisher"; @@ -27,10 +26,12 @@ export type ClientRequestOptions = { timeout?: number; }; +export type ApplicationObject = { name: string; version: number | string }; + export type ClientOptions = { agent?: string; apiKey: string; - application: string | { name: string; version: number }; + application: string | ApplicationObject; endpoint: string; environment: string; retries?: number; @@ -38,18 +39,18 @@ export type ClientOptions = { keepalive?: boolean; }; +type NormalizedClientOptions = Omit, "application"> & { + application: ApplicationObject; +}; + export default class Client { - private readonly _opts: ClientOptions; + private readonly _opts: NormalizedClientOptions; private readonly _delay: number; constructor(opts: ClientOptions) { - this._opts = Object.assign( + const merged: Record = Object.assign( { agent: "javascript-client", - apiKey: undefined, - application: undefined, - endpoint: undefined, - environment: undefined, retries: 5, timeout: 3000, keepalive: true, @@ -57,12 +58,12 @@ export default class Client { opts ); - for (const key of ["agent", "application", "apiKey", "endpoint", "environment"] as const) { - if (key in this._opts && this._opts[key] !== undefined) { - const value = this._opts[key]; - if (typeof value !== "string" || value.length === 0) { + for (const key of ["agent", "application", "apiKey", "endpoint", "environment"]) { + if (key in merged && merged[key] !== undefined) { + const value = merged[key]; + if (typeof value !== "string" || (value as string).length === 0) { if (key === "application") { - if (value !== null && typeof value === "object" && "name" in value) { + if (value !== null && typeof value === "object" && "name" in (value as object)) { continue; } } @@ -73,13 +74,14 @@ export default class Client { } } - if (typeof this._opts.application === "string") { - this._opts.application = { - name: this._opts.application, + if (typeof merged.application === "string") { + merged.application = { + name: merged.application, version: 0, }; } + this._opts = merged as unknown as NormalizedClientOptions; this._delay = 50; } @@ -88,7 +90,7 @@ export default class Client { ...options, path: "/context", query: { - application: getApplicationName(this._opts.application), + application: this._opts.application.name, environment: this._opts.environment, }, }); @@ -111,6 +113,7 @@ export default class Client { units: params.units, hashed: params.hashed, publishedAt: params.publishedAt || Date.now(), + sdkVersion: params.sdkVersion, }; if (Array.isArray(params.goals) && params.goals.length > 0) { @@ -160,8 +163,8 @@ export default class Client { "X-API-Key": this._opts.apiKey, "X-Agent": this._opts.agent, "X-Environment": this._opts.environment, - "X-Application": getApplicationName(this._opts.application), - "X-Application-Version": getApplicationVersion(this._opts.application), + "X-Application": this._opts.application.name, + "X-Application-Version": this._opts.application.version, }; } @@ -287,6 +290,18 @@ export default class Client { }); } + getAgent(): string { + return this._opts.agent; + } + + getApplication(): ApplicationObject { + return this._opts.application; + } + + getEnvironment(): string { + return this._opts.environment; + } + getUnauthed(options: ClientRequestOptions) { return this.request({ ...options, diff --git a/src/context.ts b/src/context.ts index 424a0b4..c862b3d 100644 --- a/src/context.ts +++ b/src/context.ts @@ -6,6 +6,7 @@ import SDK, { EventLogger, EventName } from "./sdk"; import { ContextPublisher, PublishParams } from "./publisher"; import { ContextDataProvider } from "./provider"; import { ClientRequestOptions } from "./client"; +import { SDK_VERSION } from "./version"; type JSONPrimitive = string | number | boolean | null; type JSONObject = { [key: string]: JSONValue }; @@ -115,6 +116,7 @@ export type ContextOptions = { eventLogger?: EventLogger; refreshPeriod: number; publishDelay: number; + includeSystemAttributes?: boolean; }; export type ContextData = { @@ -785,6 +787,31 @@ export default class Context { } } + private _buildAttributes(): Attribute[] { + const allAttributes: Attribute[] = []; + + if (this._opts.includeSystemAttributes === true) { + const client = this._sdk.getClient(); + const app = client.getApplication(); + const now = Date.now(); + allAttributes.push( + { name: "sdk_name", value: client.getAgent(), setAt: now }, + { name: "sdk_version", value: SDK_VERSION, setAt: now }, + { name: "application", value: app.name, setAt: now }, + { name: "environment", value: client.getEnvironment(), setAt: now } + ); + if ((typeof app.version === "string" && app.version.length > 0) || (typeof app.version === "number" && app.version > 0)) { + allAttributes.push({ name: "app_version", value: app.version, setAt: now }); + } + } + + for (const x of this._attrs) { + allAttributes.push({ name: x.name, value: x.value, setAt: x.setAt }); + } + + return allAttributes; + } + private _flush(callback?: (error?: Error) => void, requestOptions?: ClientRequestOptions) { if (this._publishTimeout !== undefined) { clearTimeout(this._publishTimeout); @@ -797,63 +824,69 @@ export default class Context { } } else { if (!this._failed) { - const request: PublishParams = { - publishedAt: Date.now(), - units: Object.entries(this._units).map((entry) => ({ - type: entry[0], - uid: this._unitHash(entry[0]), - })), - hashed: true, - }; - - if (this._goals.length > 0) { - request.goals = this._goals.map((x) => ({ - name: x.name, - achievedAt: x.achievedAt, - properties: x.properties, - })); - } + try { + const request: PublishParams = { + publishedAt: Date.now(), + units: Object.entries(this._units).map((entry) => ({ + type: entry[0], + uid: this._unitHash(entry[0]), + })), + hashed: true, + sdkVersion: SDK_VERSION, + }; + + if (this._goals.length > 0) { + request.goals = this._goals.map((x) => ({ + name: x.name, + achievedAt: x.achievedAt, + properties: x.properties, + })); + } - if (this._exposures.length > 0) { - request.exposures = this._exposures.map((x) => ({ - id: x.id, - name: x.name, - unit: x.unit, - exposedAt: x.exposedAt, - variant: x.variant, - assigned: x.assigned, - eligible: x.eligible, - overridden: x.overridden, - fullOn: x.fullOn, - custom: x.custom, - audienceMismatch: x.audienceMismatch, - })); - } + if (this._exposures.length > 0) { + request.exposures = this._exposures.map((x) => ({ + id: x.id, + name: x.name, + unit: x.unit, + exposedAt: x.exposedAt, + variant: x.variant, + assigned: x.assigned, + eligible: x.eligible, + overridden: x.overridden, + fullOn: x.fullOn, + custom: x.custom, + audienceMismatch: x.audienceMismatch, + })); + } - if (this._attrs.length > 0) { - request.attributes = this._attrs.map((x) => ({ - name: x.name, - value: x.value, - setAt: x.setAt, - })); - } + const allAttributes = this._buildAttributes(); + if (allAttributes.length > 0) { + request.attributes = allAttributes; + } - this._publisher - .publish(request, this._sdk, this, requestOptions) - .then(() => { - this._logEvent("publish", request); + this._publisher + .publish(request, this._sdk, this, requestOptions) + .then(() => { + this._logEvent("publish", request); - if (typeof callback === "function") { - callback(); - } - }) - .catch((e: Error) => { - this._logError(e); + if (typeof callback === "function") { + callback(); + } + }) + .catch((e: Error) => { + this._logError(e); - if (typeof callback === "function") { - callback(e); - } - }); + if (typeof callback === "function") { + callback(e); + } + }); + } catch (e) { + this._logError(e as Error); + + if (typeof callback === "function") { + callback(e as Error); + } + } } else { if (typeof callback === "function") { callback(); diff --git a/src/publisher.ts b/src/publisher.ts index 8e0cc64..79c5f14 100644 --- a/src/publisher.ts +++ b/src/publisher.ts @@ -6,6 +6,7 @@ export type PublishParams = { units: Unit[]; publishedAt: number; hashed: boolean; + sdkVersion: string; attributes?: Attribute[]; goals?: Goal[]; exposures?: Exposure[]; diff --git a/src/utils.ts b/src/utils.ts index b2eaf42..529d486 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,9 @@ import { md5 } from "./md5"; -export const getApplicationName = (app: string | { name: string; version: number }): string => +export const getApplicationName = (app: string | { name: string; version: number | string }): string => typeof app !== "string" ? app.name : app; -export const getApplicationVersion = (app: string | { name: string; version: number }): number => +export const getApplicationVersion = (app: string | { name: string; version: number | string }): number | string => typeof app !== "string" ? app.version : 0; function isBrowser() { diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..9bcd66f --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const SDK_VERSION = "1.13.4";