diff --git a/e2e/core/node-integration.test.ts b/e2e/core/node-integration.test.ts index b239791..d86f938 100644 --- a/e2e/core/node-integration.test.ts +++ b/e2e/core/node-integration.test.ts @@ -1,4 +1,4 @@ -import { TrackJS, timestamp } from "@trackjs/core"; +import { TrackJS, timestamp, userAgent } from "@trackjs/core"; import { test, expect, beforeEach } from "vitest"; import { MockTransport } from "./mocks/transport"; import type { NetworkTelemetry, Transport, TransportRequest, TransportResponse } from "@trackjs/core"; @@ -49,6 +49,35 @@ test('TrackJS.track() can track errors after install', async () => { }); }); +test('TrackJS.track() with environment', async () => { + const transport = new MockTransport(); + + TrackJS.initialize({ + token: 'test token', + transport, + dependencies: { + "foo": "1.2.3" + }, + originalUrl: "original-url", + referrerUrl: "referrer-url", + userAgent: userAgent("Node", "12.1", "windows", "x64", "11.2"), + }); + + await TrackJS.track(new Error('Oops')); + + expect(transport.sentRequests).toHaveLength(1); + expect(JSON.parse(transport.sentRequests[0]?.data as string)).toMatchObject({ + environment: expect.objectContaining({ + originalUrl: "original-url", + referrer: "referrer-url", + dependencies: { + "foo": "1.2.3" + }, + userAgent: "Node/12.1 (windows x64 11.2)" + }) + }); +}) + test('TrackJS.track() with custom metadata', async () => { const transport = new MockTransport(); diff --git a/packages/core/package.json b/packages/core/package.json index 6fe9393..a5c575d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -43,6 +43,6 @@ "clean": "rimraf dist", "prepublishOnly": "npm run build", "test": "vitest run", - "test:watch": "vitest" + "dev": "vitest" } } diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index ed00ca7..9edccea 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,7 +1,6 @@ import { Metadata } from "./metadata"; import { TelemetryLog } from "./telemetryLog"; import { timestamp, serialize, isError } from "./utils"; - import type { CapturePayload, Options, @@ -75,14 +74,12 @@ export class Client { environment: { age: 0, - dependencies: { - "foo": "bar" - }, - originalUrl: "", - referrer: "", - userAgent: 'node/22.0 (osx x64 123)', - viewportHeight: -1, - viewportWidth: -1 + dependencies: structuredClone(this.options.dependencies), + originalUrl: this.options.originalUrl, + referrer: this.options.referrerUrl, + userAgent: this.options.userAgent, + viewportHeight: this.options.viewportHeight, + viewportWidth: this.options.viewportWidth }, metadata: payloadMetadata.get(), @@ -92,8 +89,8 @@ export class Client { network: this.telemetry.get("net"), visitor: this.telemetry.get("vis"), - agentPlatform: "", - version: '0.0.0', + agentPlatform: this.options.agent, + version: this.options.agentVersion, throttled: 0, }; } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts new file mode 100644 index 0000000..389c1a6 --- /dev/null +++ b/packages/core/src/constants.ts @@ -0,0 +1,6 @@ +export const MAX_METADATA_LENGTH = 500; +export const MAX_TELEMETRY_LOG_SIZE = 30; +export const MAX_TELEMETRY_MESSAGE_LENGTH = 10_000; +export const MAX_TELEMETRY_URI_LENGTH = 1_000; +export const MAX_TELEMETRY_ATTRIBUTES = 20; +export const MAX_TELEMETRY_ATTRIBUTE_VALUE = 500; \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a95ba72..3e1087e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ export * as TrackJS from './trackjs'; -export { timestamp, uuid } from './utils/'; +export { timestamp, userAgent, uuid } from './utils/'; export type * from './types/'; \ No newline at end of file diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 4ae8d24..a01777e 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -1,7 +1,6 @@ +import { MAX_METADATA_LENGTH } from "./constants"; import { truncate } from "./utils"; -const MAX_METADATA_LENGTH = 500; - export class Metadata { private store: Map = new Map(); @@ -30,9 +29,7 @@ export class Metadata { public clone(): Metadata { const cloned = new Metadata(); - for (const [key, value] of this.store.entries()) { - cloned.store.set(key, value); - } + cloned.store = structuredClone(this.store); return cloned; } diff --git a/packages/core/src/telemetryLog.ts b/packages/core/src/telemetryLog.ts index 31b4e5c..9a0bd30 100644 --- a/packages/core/src/telemetryLog.ts +++ b/packages/core/src/telemetryLog.ts @@ -1,11 +1,21 @@ -import type { ConsoleTelemetry, NavigationTelemetry, NetworkTelemetry, Telemetry, TelemetryType, VisitorTelemetry } from "./types"; import { truncate } from "./utils"; +import { + MAX_TELEMETRY_ATTRIBUTE_VALUE, + MAX_TELEMETRY_ATTRIBUTES, + MAX_TELEMETRY_LOG_SIZE, + MAX_TELEMETRY_MESSAGE_LENGTH, + MAX_TELEMETRY_URI_LENGTH +} from "./constants"; + +import type { + ConsoleTelemetry, + NavigationTelemetry, + NetworkTelemetry, + Telemetry, + TelemetryType, + VisitorTelemetry +} from "./types"; -const MAX_LOG_SIZE = 30; -const MAX_MESSAGE_LENGTH = 10_000; -const MAX_URI_LENGTH = 1_000; -const MAX_ATTRIBUTES = 20; -const MAX_ATTRIBUTE_VALUE = 500; export class TelemetryLog { @@ -36,8 +46,8 @@ export class TelemetryLog { } this.store.push({ type, telemetry}); - if (this.store.length > MAX_LOG_SIZE) { - this.store = this.store.slice(this.store.length - MAX_LOG_SIZE); + if (this.store.length > MAX_TELEMETRY_LOG_SIZE) { + this.store = this.store.slice(this.store.length - MAX_TELEMETRY_LOG_SIZE); } } @@ -47,7 +57,7 @@ export class TelemetryLog { public clone(): TelemetryLog { const cloned = new TelemetryLog(); - cloned.store = this.store.slice(0); + cloned.store = structuredClone(this.store); return cloned; } @@ -64,18 +74,18 @@ export class TelemetryLog { } export function _normalizeConsoleTelemetry(telemetry: any) : ConsoleTelemetry { - telemetry.message = truncate(telemetry.message, MAX_MESSAGE_LENGTH); + telemetry.message = truncate(telemetry.message, MAX_TELEMETRY_MESSAGE_LENGTH); return telemetry; } export function _normalizeNavigationTelemetry(telemetry: any) : NavigationTelemetry { - telemetry.from = truncate(telemetry.from, MAX_URI_LENGTH); - telemetry.to = truncate(telemetry.to, MAX_URI_LENGTH); + telemetry.from = truncate(telemetry.from, MAX_TELEMETRY_URI_LENGTH); + telemetry.to = truncate(telemetry.to, MAX_TELEMETRY_URI_LENGTH); return telemetry; } export function _normalizeNetworkTelemetry(telemetry: any) : NetworkTelemetry { - telemetry.url = truncate(telemetry.url, MAX_URI_LENGTH); + telemetry.url = truncate(telemetry.url, MAX_TELEMETRY_URI_LENGTH); return telemetry; } @@ -84,10 +94,10 @@ export function _normalizeVisitorTelemetry(telemetry: any) : VisitorTelemetry { const currentAttributes: attributes = telemetry.element?.attributes || {}; let normalizedAttributes: attributes = {}; - const limitedAttributes = Object.entries(currentAttributes).slice(0, MAX_ATTRIBUTES); + const limitedAttributes = Object.entries(currentAttributes).slice(0, MAX_TELEMETRY_ATTRIBUTES); for (const [key, value] of limitedAttributes) { - normalizedAttributes[key] = truncate(value, MAX_ATTRIBUTE_VALUE); + normalizedAttributes[key] = truncate(value, MAX_TELEMETRY_ATTRIBUTE_VALUE); } (telemetry.element || {}).attributes = normalizedAttributes; diff --git a/packages/core/src/trackjs.ts b/packages/core/src/trackjs.ts index 87caba7..970f3d2 100644 --- a/packages/core/src/trackjs.ts +++ b/packages/core/src/trackjs.ts @@ -18,17 +18,25 @@ let config: Options | null = null; let client: Client | null = null; const defaultOptions: Options = { - application: '', + agent: "core", + agentVersion: "{{AGENT_VERSION}}", + application: "", + dependencies: {}, correlationId: uuid(), - errorURL: 'https://capture.trackjs.com/capture/node', + errorURL: "https://capture.trackjs.com/capture/node", metadata: {}, onError: () => true, + originalUrl: "", + referrerUrl: "", serializer: [], - sessionId: '', - token: '', + sessionId: "", + token: "", transport: new FetchTransport(), - userId: '', - version: '' + userAgent: "", + userId: "", + version: "", + viewportHeight: -1, + viewportWidth: -1 }; /** @@ -74,7 +82,7 @@ export function initialize(options: Partial & { token: string }): void } /** - * Adds a object set of key-value pairs to metadata for any future errors. + * Adds a object map of key-value pairs to metadata for any future errors. * Keys and values will be truncated to 500 characters. * * @param metadata - object with string values to be added as metadata. @@ -163,10 +171,6 @@ export function addTelemetry(type: TelemetryType, telemetry: Telemetry): void { return client!.addTelemetry(type, telemetry); } -export function addDependencies(...args: [dependencies: Record]): void { - throw new Error("not implemented"); -} - /** * Track and error or error-like object to the TrackJS error monitoring service. * diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 64fef65..1975213 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -39,6 +39,20 @@ export interface TrackOptions { export interface Options { + /** + * The name of the TrackJS agent package + * + * @default "core" + */ + agent: string, + + /** + * The version of the TrackJS agent package + * + * @default "{{AGENT_VERSION}}" + */ + agentVersion: string, + /** * TrackJS Application key. * @@ -53,6 +67,14 @@ export interface Options { */ correlationId: string; + /** + * Dependency package names and version for the current environment. + * + * @default {} + * @example { "node": "22.12.0" } + */ + dependencies: { [name: string]: string }; + /** * URL destination override for capturing errors. * @@ -69,15 +91,32 @@ export interface Options { /** * Custom handler to manipulate or suppress errors captured by the agent. + * * @param payload error payload to be sent to TrackJS. * @returns false will suppress the error from being sent. - * * @default (payload) => true */ onError: (payload: CapturePayload) => boolean; /** - * Custom functions for serializing objects to strings. Will execute before the default serializer. + * When environment has a user interface, the URI location where the application + * was started, or the URL of the page when it was first loaded. + * + * @default "" + */ + originalUrl: string + + /** + * When environment has a user interface, the URI location where the user came + * from before landing on this page or screen. + * + * @default "" + */ + referrerUrl: string + + /** + * Custom functions for serializing objects to strings. Will execute before the + * default serializer. * * @default [] */ @@ -96,12 +135,23 @@ export interface Options { token: string; /** - * Custom transport function for sending data. Required if the current environment does not support `fetch`. + * Custom transport function for sending data. Required if the current + * environment does not support `fetch`. * * @default FetchTransport */ transport: Transport; + /** + * User-Agent string describing the user's running environment, browser, process, + * operating system, arch, and version. For non-browser environments, this can + * be constructed with the userAgent() util function. + * + * @see {@link userAgent} + * @defaults "" + */ + userAgent: string + /** * Customer-generated Id representing the current user. * @@ -116,4 +166,19 @@ export interface Options { */ version: string; + /** + * When environment has a user interface, the height of the user viewport in + * pixels. + * + * @default -1 + */ + viewportHeight: number + + /** + * When environment has a user interface, the width of the user viewport in + * pixels. + * + * @default -1 + */ + viewportWidth: number } \ No newline at end of file diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 9475c79..97e8a84 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -2,4 +2,5 @@ export * from './isType'; export * from './serialize'; export * from './timestamp'; export * from './truncate'; +export * from './userAgent'; export * from './uuid'; \ No newline at end of file diff --git a/packages/core/src/utils/userAgent.ts b/packages/core/src/utils/userAgent.ts new file mode 100644 index 0000000..2ce2b0f --- /dev/null +++ b/packages/core/src/utils/userAgent.ts @@ -0,0 +1,26 @@ +/** + * Build a TrackJS-compatible browser-like userAgent string based on the current + * running environment. + * + * @param engine - The runtime engine, like "Node", "Deno", or "Chrome" + * @param engineVersion - The runtime version. + * @param os - The running operating system, like "Macintosh" or "Windows" + * @param osArch - The architecture of the operating system, like "x64" + * @param osVersion - The running operating system version, like 10.15.7 + * @returns A constructed userAgent String + * + * @example + * ``` + * TrackJS.initialize({ + * token: "your-token", + * userAgent: userAgent("Node", "22.12.0", "Macintosh", "x64", "15.6") + * }) + */ +export function userAgent( + engine: string, + engineVersion: string, + os: string, + osArch: string, + osVersion: string) : string { + return `${engine}/${engineVersion.replace("v", "")} (${os} ${osArch} ${osVersion})`; +} \ No newline at end of file diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts index ab0d230..80b8695 100644 --- a/packages/core/test/client.test.ts +++ b/packages/core/test/client.test.ts @@ -10,17 +10,27 @@ let defaultOptions: Options; beforeEach(() => { mockTransport = new MockTransport(); defaultOptions = { + agent: "test", + agentVersion: "1.2.3", application: 'test-app', correlationId: 'test-correlation-id', + dependencies: { + "foo": "1.2.3" + }, errorURL: 'https://test.trackjs.com/capture', metadata: {}, onError: () => true, + originalUrl: "original-url", + referrerUrl: "referrer-url", serializer: [], sessionId: 'test-session', token: 'test-token', transport: mockTransport, + userAgent: "test-agent", userId: 'test-user', - version: '0.0.0' + version: '0.0.0', + viewportHeight: 100, + viewportWidth: 200 }; }); @@ -54,13 +64,13 @@ describe("_createPayload()", () => { environment: { age: 0, dependencies: { - "foo": "bar" + "foo": "1.2.3" }, - originalUrl: "", - referrer: "", - userAgent: 'node/22.0 (osx x64 123)', - viewportHeight: -1, - viewportWidth: -1 + originalUrl: "original-url", + referrer: "referrer-url", + userAgent: "test-agent", + viewportHeight: 100, + viewportWidth: 200, }, metadata: [], @@ -69,8 +79,8 @@ describe("_createPayload()", () => { network: [], visitor: [], - agentPlatform: "", - version: '0.0.0', + agentPlatform: "test", + version: "1.2.3", throttled: 0 }); }); diff --git a/packages/core/test/utils/userAgent.test.ts b/packages/core/test/utils/userAgent.test.ts new file mode 100644 index 0000000..a8f90a1 --- /dev/null +++ b/packages/core/test/utils/userAgent.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest'; +import { userAgent } from '../../src/utils'; + +test('Constructs expected userAgent String', () => { + const result = userAgent("Node", "v22.12.0", "darwin", "arm64", "24.6.0"); + expect(result).toStrictEqual("Node/22.12.0 (darwin arm64 24.6.0)"); +});