neatlogs 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.cjs CHANGED
@@ -34,9 +34,9 @@ var Neatlogs = class {
34
34
  baseUrl;
35
35
  enabled;
36
36
  onError;
37
- endUser;
37
+ endUserId;
38
38
  endUserMetadata;
39
- session;
39
+ sessionId;
40
40
  constructor(opts) {
41
41
  if (!opts || !opts.apiKey) {
42
42
  throw new Error("Neatlogs: apiKey is required");
@@ -46,9 +46,9 @@ var Neatlogs = class {
46
46
  const endpoint = opts.endpoint || DEFAULT_ENDPOINT;
47
47
  this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;
48
48
  this.enabled = opts.enabled !== false;
49
- this.endUser = opts.endUser;
49
+ this.endUserId = opts.endUserId;
50
50
  this.endUserMetadata = opts.endUserMetadata;
51
- this.session = opts.session;
51
+ this.sessionId = opts.sessionId;
52
52
  this.onError = opts.onError || ((err) => {
53
53
  if (typeof console !== "undefined") console.warn("[neatlogs] send failed:", err);
54
54
  });
@@ -85,17 +85,17 @@ var Neatlogs = class {
85
85
  }
86
86
  /**
87
87
  * Fold identity (end-user + session) onto the trace ROOT as canonical
88
- * attributes, and strip the convenience `endUser`/`endUserMetadata`/`session`
88
+ * attributes, and strip the convenience `endUserId`/`endUserMetadata`/`sessionId`
89
89
  * fields so they aren't sent as raw root fields. Per-call values win over the
90
90
  * client defaults; explicit `attributes` win over both. Only the root carries
91
91
  * identity — one end-user per trace, one session per trace.
92
92
  */
93
93
  applyIdentity(body) {
94
- const { endUser, endUserMetadata, session, ...rest } = body;
95
- const id = endUser ?? this.endUser;
94
+ const { endUserId, endUserMetadata, sessionId, ...rest } = body;
95
+ const id = endUserId ?? this.endUserId;
96
96
  const meta = endUserMetadata ?? this.endUserMetadata;
97
- const sessionId = session ?? this.session;
98
- if (id === void 0 && meta === void 0 && sessionId === void 0) {
97
+ const session = sessionId ?? this.sessionId;
98
+ if (id === void 0 && meta === void 0 && session === void 0) {
99
99
  return rest;
100
100
  }
101
101
  const attributes = { ...rest.attributes ?? {} };
@@ -105,8 +105,8 @@ var Neatlogs = class {
105
105
  if (meta !== void 0 && attributes[END_USER_METADATA_KEY] === void 0) {
106
106
  attributes[END_USER_METADATA_KEY] = typeof meta === "string" ? meta : JSON.stringify(meta);
107
107
  }
108
- if (sessionId !== void 0 && attributes[SESSION_ID_KEY] === void 0) {
109
- attributes[SESSION_ID_KEY] = String(sessionId);
108
+ if (session !== void 0 && attributes[SESSION_ID_KEY] === void 0) {
109
+ attributes[SESSION_ID_KEY] = String(session);
110
110
  }
111
111
  return { ...rest, attributes };
112
112
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Neatlogs Browser SDK — `neatlogs/browser`\n *\n * A minimal, browser-safe client for sending traces to Neatlogs from web apps.\n * It has ZERO dependencies (no OpenTelemetry, no Node APIs) — only `fetch` — so\n * it bundles cleanly into front-end apps. It POSTs plain JSON to the backend's\n * simple trace endpoint (`/v1/trace`); the backend generates trace/span ids,\n * builds the hierarchy from nesting, infers cost from model+tokens, and pushes\n * through the normal pipeline. Nothing here streams OTLP.\n *\n * Usage:\n * import { Neatlogs } from 'neatlogs/browser';\n * const nl = new Neatlogs({ apiKey: 'nl_...' });\n *\n * // one-shot AI interaction\n * await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output,\n * tokens: { prompt: 10, completion: 5 } });\n *\n * // a full nested trace (same shape the backend's POST /v1/trace accepts)\n * await nl.trace({ name: 'support-chat', children: [\n * { name: 'retrieve', query, documents },\n * { name: 'answer', model: 'gpt-4o', input, output },\n * ]});\n *\n * // streaming: open, accumulate, finish\n * const t = nl.startTrace({ name: 'chat', model: 'gpt-4o', input });\n * t.finish({ output: full, tokens: { prompt, completion } });\n */\n\nconst DEFAULT_ENDPOINT = \"https://ingest.neatlogs.com\";\n\n// Canonical identity attribute keys (inlined — this file stays dependency-free).\nconst END_USER_ID_KEY = \"neatlogs.end_user.id\";\nconst END_USER_METADATA_KEY = \"neatlogs.end_user.metadata\";\nconst SESSION_ID_KEY = \"neatlogs.session.id\";\n\n// --- the simple trace shape the backend (/v1/trace) accepts --------------------\n// Mirrors the server-side SimpleSpan; kept local so this file has no imports.\n\nexport interface NeatlogsLog {\n level?: string;\n message: string;\n timestamp?: string;\n}\n\n/**\n * Span kinds the backend accepts (the canonical set). `kind` is optional — when\n * omitted the backend infers it from the fields present.\n */\nexport type NeatlogsKind =\n | \"WORKFLOW\" | \"AGENT\" | \"CHAIN\" | \"TOOL\" | \"RETRIEVER\" | \"RERANKER\"\n | \"EMBEDDING\" | \"LLM\" | \"GUARDRAIL\" | \"MCP_TOOL\" | \"TASK\"\n | \"VECTOR_STORE\" | \"EVALUATOR\" | \"HTTP\";\n\nexport interface NeatlogsSpan {\n name: string;\n /** Optional — the backend infers the kind from fields when omitted. */\n kind?: NeatlogsKind | string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n query?: unknown;\n documents?: unknown;\n tool_name?: string;\n passed?: boolean;\n score?: number;\n metadata?: Record<string, unknown>;\n status?: string;\n error?: string;\n start?: string;\n end?: string;\n /** Simplest way to record latency — the backend derives end from start + this. */\n duration_ms?: number;\n /**\n * Full canonical-attribute escape hatch. Send ANY neatlogs.* attribute the SDK\n * supports — e.g. { \"neatlogs.llm.temperature\": 0.7, \"neatlogs.agent.role\":\n * \"researcher\", \"neatlogs.tool.parameters\": {...} }. Non-canonical keys are\n * dropped server-side. The fields above (model/tokens/query/...) are shortcuts\n * for the common ones; explicit `attributes` win on conflict.\n */\n attributes?: Record<string, unknown>;\n children?: NeatlogsSpan[];\n logs?: NeatlogsLog[];\n /**\n * END-USER this trace belongs to — only meaningful on the ROOT of a trace\n * (overrides the client default). One end-user per trace; the backend rolls it\n * up to the trace and its session. Ignored on child spans.\n */\n endUser?: string;\n /** Arbitrary end-user fields for the trace root (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * SESSION this trace belongs to — the conversation/thread grouping many turns.\n * Only meaningful on the ROOT of a trace (overrides the client default). Every\n * trace sharing a `session` is grouped into one session in the dashboard; a new\n * trace per turn, all with the same `session`, forms a multi-turn conversation.\n * Ignored on child spans.\n */\n session?: string;\n}\n\n/** The root of a trace = a span node (its `name` becomes the workflow name). */\nexport type NeatlogsTrace = NeatlogsSpan;\n\nexport interface NeatlogsOptions {\n /**\n * Your Neatlogs WRITE key (`nlw_…`) — an ingest-only credential safe to embed in\n * browser code. (A full project key also works but should not be exposed client-side,\n * since it can read data.)\n */\n apiKey: string;\n /**\n * Project NAME to ingest into. REQUIRED when using a write key (the key identifies\n * you, not a project). Sent as the root `project` field on every trace. Ignored for\n * a full project key (already project-scoped).\n */\n project?: string;\n /** Backend base URL. Defaults to the same host the SDKs use. */\n endpoint?: string;\n /** Set false to validate calls without sending (default true). */\n enabled?: boolean;\n /** Called on transport errors instead of throwing (default: console.warn). */\n onError?: (err: unknown) => void;\n /**\n * Default END-USER identity for every trace this client sends — the user of\n * your app, not the operator. One end-user per trace; the backend rolls it up\n * to the trace and its session. A per-call `endUser` (on trace()/trackAI())\n * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUser })`.\n */\n endUser?: string;\n /** Default arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * Default SESSION for every trace this client sends — the conversation/thread\n * these traces belong to. A per-call `session` (on trace()/trackAI()) overrides\n * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, session: convId })`.\n */\n session?: string;\n}\n\nexport interface TrackResult {\n ok: boolean;\n trace_id?: string;\n spans?: number;\n error?: string;\n}\n\n/** Shorthand for a single AI interaction → a one-span trace. */\nexport interface TrackAIInput {\n name: string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n metadata?: Record<string, unknown>;\n duration_ms?: number;\n /** Any canonical neatlogs.* attributes (see NeatlogsSpan.attributes). */\n attributes?: Record<string, unknown>;\n /** Override the inferred kind (defaults to LLM for trackAI). */\n kind?: NeatlogsKind | string;\n /** END-USER for this trace (overrides the client default). One per trace. */\n endUser?: string;\n /** Arbitrary end-user fields for this trace (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /** SESSION this trace belongs to (overrides the client default). */\n session?: string;\n}\n\nexport class Neatlogs {\n private readonly apiKey: string;\n private readonly project?: string;\n private readonly baseUrl: string;\n private readonly enabled: boolean;\n private readonly onError: (err: unknown) => void;\n private readonly endUser?: string;\n private readonly endUserMetadata?: Record<string, unknown>;\n private readonly session?: string;\n\n constructor(opts: NeatlogsOptions) {\n if (!opts || !opts.apiKey) {\n throw new Error(\"Neatlogs: apiKey is required\");\n }\n this.apiKey = opts.apiKey;\n this.project = opts.project;\n // Use the origin of the configured endpoint (same convention as the Node SDK),\n // so passing a full /v1/traces URL or a bare host both work.\n const endpoint = opts.endpoint || DEFAULT_ENDPOINT;\n this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;\n this.enabled = opts.enabled !== false;\n this.endUser = opts.endUser;\n this.endUserMetadata = opts.endUserMetadata;\n this.session = opts.session;\n this.onError =\n opts.onError ||\n ((err) => {\n // eslint-disable-next-line no-console\n if (typeof console !== \"undefined\") console.warn(\"[neatlogs] send failed:\", err);\n });\n }\n\n /** Send a full (optionally nested) trace. Returns the backend's result. */\n async trace(trace: NeatlogsTrace): Promise<TrackResult> {\n return this.post(trace);\n }\n\n /** Send a single AI interaction as a one-span trace (kind defaults to LLM). */\n async trackAI(ai: TrackAIInput): Promise<TrackResult> {\n const { name, kind, ...rest } = ai;\n return this.post({ name, kind: kind ?? \"LLM\", ...rest });\n }\n\n /**\n * Begin a trace you'll complete later (e.g. streaming). Buffers the partial\n * input; call `.finish()` with the final output/tokens to send it. Nothing is\n * sent until `finish()`.\n */\n startTrace(initial: TrackAIInput): {\n finish: (final?: Partial<TrackAIInput>) => Promise<TrackResult>;\n } {\n const startedAt = nowIso();\n return {\n finish: (final?: Partial<TrackAIInput>) => {\n const merged: TrackAIInput = { ...initial, ...(final ?? {}) };\n const { name, kind, ...rest } = merged;\n return this.post({\n name,\n kind: kind ?? \"LLM\",\n start: startedAt,\n end: nowIso(),\n ...rest,\n });\n },\n };\n }\n\n /**\n * Fold identity (end-user + session) onto the trace ROOT as canonical\n * attributes, and strip the convenience `endUser`/`endUserMetadata`/`session`\n * fields so they aren't sent as raw root fields. Per-call values win over the\n * client defaults; explicit `attributes` win over both. Only the root carries\n * identity — one end-user per trace, one session per trace.\n */\n private applyIdentity(body: NeatlogsTrace): NeatlogsTrace {\n const { endUser, endUserMetadata, session, ...rest } = body as NeatlogsTrace & {\n endUser?: string;\n endUserMetadata?: Record<string, unknown>;\n session?: string;\n };\n const id = endUser ?? this.endUser;\n const meta = endUserMetadata ?? this.endUserMetadata;\n const sessionId = session ?? this.session;\n if (id === undefined && meta === undefined && sessionId === undefined) {\n return rest;\n }\n\n const attributes: Record<string, unknown> = { ...(rest.attributes ?? {}) };\n if (id !== undefined && attributes[END_USER_ID_KEY] === undefined) {\n attributes[END_USER_ID_KEY] = String(id);\n }\n if (meta !== undefined && attributes[END_USER_METADATA_KEY] === undefined) {\n attributes[END_USER_METADATA_KEY] =\n typeof meta === \"string\" ? meta : JSON.stringify(meta);\n }\n if (sessionId !== undefined && attributes[SESSION_ID_KEY] === undefined) {\n attributes[SESSION_ID_KEY] = String(sessionId);\n }\n return { ...rest, attributes };\n }\n\n /** POST the trace JSON to the backend's /v1/trace endpoint. */\n private async post(body: NeatlogsTrace): Promise<TrackResult> {\n if (!this.enabled) return { ok: true };\n // Fold identity (end-user + session) onto the root before anything else.\n body = this.applyIdentity(body);\n // Inject the configured project name into the root (required for write keys;\n // ignored server-side for full project keys). A `project` already on the body wins.\n const payload =\n this.project && (body as any).project === undefined\n ? { ...body, project: this.project }\n : body;\n try {\n const res = await fetch(`${this.baseUrl}/v1/trace`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(payload),\n // keepalive lets the request survive a page unload (e.g. on navigation).\n keepalive: true,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n const result = { ok: false, error: `HTTP ${res.status}${text ? `: ${text}` : \"\"}` };\n this.onError(new Error(result.error));\n return result;\n }\n const data = (await res.json().catch(() => ({}))) as Partial<TrackResult>;\n return { ok: true, trace_id: data.trace_id, spans: data.spans };\n } catch (err) {\n // Never throw into the host app over telemetry.\n this.onError(err);\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n}\n\n// --- helpers (kept local, no imports) -----------------------------------------\n\nfunction safeOrigin(endpoint: string): string | null {\n try {\n return new URL(endpoint).origin;\n } catch {\n return null;\n }\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\nexport default Neatlogs;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BA,IAAM,mBAAmB;AAGzB,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;AAC9B,IAAM,iBAAiB;AAuIhB,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAuB;AACjC,QAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ;AACzB,YAAM,IAAI,MAAM,8BAA8B;AAAA,IAChD;AACA,SAAK,SAAS,KAAK;AACnB,SAAK,UAAU,KAAK;AAGpB,UAAM,WAAW,KAAK,YAAY;AAClC,SAAK,UAAU,WAAW,QAAQ,KAAK;AACvC,SAAK,UAAU,KAAK,YAAY;AAChC,SAAK,UAAU,KAAK;AACpB,SAAK,kBAAkB,KAAK;AAC5B,SAAK,UAAU,KAAK;AACpB,SAAK,UACH,KAAK,YACJ,CAAC,QAAQ;AAER,UAAI,OAAO,YAAY,YAAa,SAAQ,KAAK,2BAA2B,GAAG;AAAA,IACjF;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,MAAM,OAA4C;AACtD,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB;AAAA;AAAA,EAGA,MAAM,QAAQ,IAAwC;AACpD,UAAM,EAAE,MAAM,MAAM,GAAG,KAAK,IAAI;AAChC,WAAO,KAAK,KAAK,EAAE,MAAM,MAAM,QAAQ,OAAO,GAAG,KAAK,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,SAET;AACA,UAAM,YAAY,OAAO;AACzB,WAAO;AAAA,MACL,QAAQ,CAAC,UAAkC;AACzC,cAAM,SAAuB,EAAE,GAAG,SAAS,GAAI,SAAS,CAAC,EAAG;AAC5D,cAAM,EAAE,MAAM,MAAM,GAAG,KAAK,IAAI;AAChC,eAAO,KAAK,KAAK;AAAA,UACf;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,OAAO;AAAA,UACP,KAAK,OAAO;AAAA,UACZ,GAAG;AAAA,QACL,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,MAAoC;AACxD,UAAM,EAAE,SAAS,iBAAiB,SAAS,GAAG,KAAK,IAAI;AAKvD,UAAM,KAAK,WAAW,KAAK;AAC3B,UAAM,OAAO,mBAAmB,KAAK;AACrC,UAAM,YAAY,WAAW,KAAK;AAClC,QAAI,OAAO,UAAa,SAAS,UAAa,cAAc,QAAW;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,aAAsC,EAAE,GAAI,KAAK,cAAc,CAAC,EAAG;AACzE,QAAI,OAAO,UAAa,WAAW,eAAe,MAAM,QAAW;AACjE,iBAAW,eAAe,IAAI,OAAO,EAAE;AAAA,IACzC;AACA,QAAI,SAAS,UAAa,WAAW,qBAAqB,MAAM,QAAW;AACzE,iBAAW,qBAAqB,IAC9B,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,IAAI;AAAA,IACzD;AACA,QAAI,cAAc,UAAa,WAAW,cAAc,MAAM,QAAW;AACvE,iBAAW,cAAc,IAAI,OAAO,SAAS;AAAA,IAC/C;AACA,WAAO,EAAE,GAAG,MAAM,WAAW;AAAA,EAC/B;AAAA;AAAA,EAGA,MAAc,KAAK,MAA2C;AAC5D,QAAI,CAAC,KAAK,QAAS,QAAO,EAAE,IAAI,KAAK;AAErC,WAAO,KAAK,cAAc,IAAI;AAG9B,UAAM,UACJ,KAAK,WAAY,KAAa,YAAY,SACtC,EAAE,GAAG,MAAM,SAAS,KAAK,QAAQ,IACjC;AACN,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,aAAa;AAAA,QAClD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,MAAM;AAAA,QACtC;AAAA,QACA,MAAM,KAAK,UAAU,OAAO;AAAA;AAAA,QAE5B,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,cAAM,SAAS,EAAE,IAAI,OAAO,OAAO,QAAQ,IAAI,MAAM,GAAG,OAAO,KAAK,IAAI,KAAK,EAAE,GAAG;AAClF,aAAK,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACpC,eAAO;AAAA,MACT;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,aAAO,EAAE,IAAI,MAAM,UAAU,KAAK,UAAU,OAAO,KAAK,MAAM;AAAA,IAChE,SAAS,KAAK;AAEZ,WAAK,QAAQ,GAAG;AAChB,aAAO,EAAE,IAAI,OAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,IAC9E;AAAA,EACF;AACF;AAIA,SAAS,WAAW,UAAiC;AACnD,MAAI;AACF,WAAO,IAAI,IAAI,QAAQ,EAAE;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,SAAiB;AACxB,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAEA,IAAO,kBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Neatlogs Browser SDK — `neatlogs/browser`\n *\n * A minimal, browser-safe client for sending traces to Neatlogs from web apps.\n * It has ZERO dependencies (no OpenTelemetry, no Node APIs) — only `fetch` — so\n * it bundles cleanly into front-end apps. It POSTs plain JSON to the backend's\n * simple trace endpoint (`/v1/trace`); the backend generates trace/span ids,\n * builds the hierarchy from nesting, infers cost from model+tokens, and pushes\n * through the normal pipeline. Nothing here streams OTLP.\n *\n * Usage:\n * import { Neatlogs } from 'neatlogs/browser';\n * const nl = new Neatlogs({ apiKey: 'nl_...' });\n *\n * // one-shot AI interaction\n * await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output,\n * tokens: { prompt: 10, completion: 5 } });\n *\n * // a full nested trace (same shape the backend's POST /v1/trace accepts)\n * await nl.trace({ name: 'support-chat', children: [\n * { name: 'retrieve', query, documents },\n * { name: 'answer', model: 'gpt-4o', input, output },\n * ]});\n *\n * // streaming: open, accumulate, finish\n * const t = nl.startTrace({ name: 'chat', model: 'gpt-4o', input });\n * t.finish({ output: full, tokens: { prompt, completion } });\n */\n\nconst DEFAULT_ENDPOINT = \"https://ingest.neatlogs.com\";\n\n// Canonical identity attribute keys (inlined — this file stays dependency-free).\nconst END_USER_ID_KEY = \"neatlogs.end_user.id\";\nconst END_USER_METADATA_KEY = \"neatlogs.end_user.metadata\";\nconst SESSION_ID_KEY = \"neatlogs.session.id\";\n\n// --- the simple trace shape the backend (/v1/trace) accepts --------------------\n// Mirrors the server-side SimpleSpan; kept local so this file has no imports.\n\nexport interface NeatlogsLog {\n level?: string;\n message: string;\n timestamp?: string;\n}\n\n/**\n * Span kinds the backend accepts (the canonical set). `kind` is optional — when\n * omitted the backend infers it from the fields present.\n */\nexport type NeatlogsKind =\n | \"WORKFLOW\" | \"AGENT\" | \"CHAIN\" | \"TOOL\" | \"RETRIEVER\" | \"RERANKER\"\n | \"EMBEDDING\" | \"LLM\" | \"GUARDRAIL\" | \"MCP_TOOL\" | \"TASK\"\n | \"VECTOR_STORE\" | \"EVALUATOR\" | \"HTTP\";\n\nexport interface NeatlogsSpan {\n name: string;\n /** Optional — the backend infers the kind from fields when omitted. */\n kind?: NeatlogsKind | string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n query?: unknown;\n documents?: unknown;\n tool_name?: string;\n passed?: boolean;\n score?: number;\n metadata?: Record<string, unknown>;\n status?: string;\n error?: string;\n start?: string;\n end?: string;\n /** Simplest way to record latency — the backend derives end from start + this. */\n duration_ms?: number;\n /**\n * Full canonical-attribute escape hatch. Send ANY neatlogs.* attribute the SDK\n * supports — e.g. { \"neatlogs.llm.temperature\": 0.7, \"neatlogs.agent.role\":\n * \"researcher\", \"neatlogs.tool.parameters\": {...} }. Non-canonical keys are\n * dropped server-side. The fields above (model/tokens/query/...) are shortcuts\n * for the common ones; explicit `attributes` win on conflict.\n */\n attributes?: Record<string, unknown>;\n children?: NeatlogsSpan[];\n logs?: NeatlogsLog[];\n /**\n * END-USER this trace belongs to — only meaningful on the ROOT of a trace\n * (overrides the client default). One end-user per trace; the backend rolls it\n * up to the trace and its session. Ignored on child spans.\n */\n endUserId?: string;\n /** Arbitrary end-user fields for the trace root (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * SESSION this trace belongs to — the conversation/thread grouping many turns.\n * Only meaningful on the ROOT of a trace (overrides the client default). Every\n * trace sharing a `sessionId` is grouped into one session in the dashboard; a new\n * trace per turn, all with the same `sessionId`, forms a multi-turn conversation.\n * Ignored on child spans.\n */\n sessionId?: string;\n}\n\n/** The root of a trace = a span node (its `name` becomes the workflow name). */\nexport type NeatlogsTrace = NeatlogsSpan;\n\nexport interface NeatlogsOptions {\n /**\n * Your Neatlogs WRITE key (`nlw_…`) — an ingest-only credential safe to embed in\n * browser code. (A full project key also works but should not be exposed client-side,\n * since it can read data.)\n */\n apiKey: string;\n /**\n * Project NAME to ingest into. REQUIRED when using a write key (the key identifies\n * you, not a project). Sent as the root `project` field on every trace. Ignored for\n * a full project key (already project-scoped).\n */\n project?: string;\n /** Backend base URL. Defaults to the same host the SDKs use. */\n endpoint?: string;\n /** Set false to validate calls without sending (default true). */\n enabled?: boolean;\n /** Called on transport errors instead of throwing (default: console.warn). */\n onError?: (err: unknown) => void;\n /**\n * Default END-USER identity for every trace this client sends — the user of\n * your app, not the operator. One end-user per trace; the backend rolls it up\n * to the trace and its session. A per-call `endUserId` (on trace()/trackAI())\n * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUserId })`.\n */\n endUserId?: string;\n /** Default arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * Default SESSION for every trace this client sends — the conversation/thread\n * these traces belong to. A per-call `sessionId` (on trace()/trackAI()) overrides\n * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, sessionId: convId })`.\n */\n sessionId?: string;\n}\n\nexport interface TrackResult {\n ok: boolean;\n trace_id?: string;\n spans?: number;\n error?: string;\n}\n\n/** Shorthand for a single AI interaction → a one-span trace. */\nexport interface TrackAIInput {\n name: string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n metadata?: Record<string, unknown>;\n duration_ms?: number;\n /** Any canonical neatlogs.* attributes (see NeatlogsSpan.attributes). */\n attributes?: Record<string, unknown>;\n /** Override the inferred kind (defaults to LLM for trackAI). */\n kind?: NeatlogsKind | string;\n /** END-USER for this trace (overrides the client default). One per trace. */\n endUserId?: string;\n /** Arbitrary end-user fields for this trace (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /** SESSION this trace belongs to (overrides the client default). */\n sessionId?: string;\n}\n\nexport class Neatlogs {\n private readonly apiKey: string;\n private readonly project?: string;\n private readonly baseUrl: string;\n private readonly enabled: boolean;\n private readonly onError: (err: unknown) => void;\n private readonly endUserId?: string;\n private readonly endUserMetadata?: Record<string, unknown>;\n private readonly sessionId?: string;\n\n constructor(opts: NeatlogsOptions) {\n if (!opts || !opts.apiKey) {\n throw new Error(\"Neatlogs: apiKey is required\");\n }\n this.apiKey = opts.apiKey;\n this.project = opts.project;\n // Use the origin of the configured endpoint (same convention as the Node SDK),\n // so passing a full /v1/traces URL or a bare host both work.\n const endpoint = opts.endpoint || DEFAULT_ENDPOINT;\n this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;\n this.enabled = opts.enabled !== false;\n this.endUserId = opts.endUserId;\n this.endUserMetadata = opts.endUserMetadata;\n this.sessionId = opts.sessionId;\n this.onError =\n opts.onError ||\n ((err) => {\n // eslint-disable-next-line no-console\n if (typeof console !== \"undefined\") console.warn(\"[neatlogs] send failed:\", err);\n });\n }\n\n /** Send a full (optionally nested) trace. Returns the backend's result. */\n async trace(trace: NeatlogsTrace): Promise<TrackResult> {\n return this.post(trace);\n }\n\n /** Send a single AI interaction as a one-span trace (kind defaults to LLM). */\n async trackAI(ai: TrackAIInput): Promise<TrackResult> {\n const { name, kind, ...rest } = ai;\n return this.post({ name, kind: kind ?? \"LLM\", ...rest });\n }\n\n /**\n * Begin a trace you'll complete later (e.g. streaming). Buffers the partial\n * input; call `.finish()` with the final output/tokens to send it. Nothing is\n * sent until `finish()`.\n */\n startTrace(initial: TrackAIInput): {\n finish: (final?: Partial<TrackAIInput>) => Promise<TrackResult>;\n } {\n const startedAt = nowIso();\n return {\n finish: (final?: Partial<TrackAIInput>) => {\n const merged: TrackAIInput = { ...initial, ...(final ?? {}) };\n const { name, kind, ...rest } = merged;\n return this.post({\n name,\n kind: kind ?? \"LLM\",\n start: startedAt,\n end: nowIso(),\n ...rest,\n });\n },\n };\n }\n\n /**\n * Fold identity (end-user + session) onto the trace ROOT as canonical\n * attributes, and strip the convenience `endUserId`/`endUserMetadata`/`sessionId`\n * fields so they aren't sent as raw root fields. Per-call values win over the\n * client defaults; explicit `attributes` win over both. Only the root carries\n * identity — one end-user per trace, one session per trace.\n */\n private applyIdentity(body: NeatlogsTrace): NeatlogsTrace {\n const { endUserId, endUserMetadata, sessionId, ...rest } = body as NeatlogsTrace & {\n endUserId?: string;\n endUserMetadata?: Record<string, unknown>;\n sessionId?: string;\n };\n const id = endUserId ?? this.endUserId;\n const meta = endUserMetadata ?? this.endUserMetadata;\n const session = sessionId ?? this.sessionId;\n if (id === undefined && meta === undefined && session === undefined) {\n return rest;\n }\n\n const attributes: Record<string, unknown> = { ...(rest.attributes ?? {}) };\n if (id !== undefined && attributes[END_USER_ID_KEY] === undefined) {\n attributes[END_USER_ID_KEY] = String(id);\n }\n if (meta !== undefined && attributes[END_USER_METADATA_KEY] === undefined) {\n attributes[END_USER_METADATA_KEY] =\n typeof meta === \"string\" ? meta : JSON.stringify(meta);\n }\n if (session !== undefined && attributes[SESSION_ID_KEY] === undefined) {\n attributes[SESSION_ID_KEY] = String(session);\n }\n return { ...rest, attributes };\n }\n\n /** POST the trace JSON to the backend's /v1/trace endpoint. */\n private async post(body: NeatlogsTrace): Promise<TrackResult> {\n if (!this.enabled) return { ok: true };\n // Fold identity (end-user + session) onto the root before anything else.\n body = this.applyIdentity(body);\n // Inject the configured project name into the root (required for write keys;\n // ignored server-side for full project keys). A `project` already on the body wins.\n const payload =\n this.project && (body as any).project === undefined\n ? { ...body, project: this.project }\n : body;\n try {\n const res = await fetch(`${this.baseUrl}/v1/trace`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(payload),\n // keepalive lets the request survive a page unload (e.g. on navigation).\n keepalive: true,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n const result = { ok: false, error: `HTTP ${res.status}${text ? `: ${text}` : \"\"}` };\n this.onError(new Error(result.error));\n return result;\n }\n const data = (await res.json().catch(() => ({}))) as Partial<TrackResult>;\n return { ok: true, trace_id: data.trace_id, spans: data.spans };\n } catch (err) {\n // Never throw into the host app over telemetry.\n this.onError(err);\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n}\n\n// --- helpers (kept local, no imports) -----------------------------------------\n\nfunction safeOrigin(endpoint: string): string | null {\n try {\n return new URL(endpoint).origin;\n } catch {\n return null;\n }\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\nexport default Neatlogs;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BA,IAAM,mBAAmB;AAGzB,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;AAC9B,IAAM,iBAAiB;AAuIhB,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAuB;AACjC,QAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ;AACzB,YAAM,IAAI,MAAM,8BAA8B;AAAA,IAChD;AACA,SAAK,SAAS,KAAK;AACnB,SAAK,UAAU,KAAK;AAGpB,UAAM,WAAW,KAAK,YAAY;AAClC,SAAK,UAAU,WAAW,QAAQ,KAAK;AACvC,SAAK,UAAU,KAAK,YAAY;AAChC,SAAK,YAAY,KAAK;AACtB,SAAK,kBAAkB,KAAK;AAC5B,SAAK,YAAY,KAAK;AACtB,SAAK,UACH,KAAK,YACJ,CAAC,QAAQ;AAER,UAAI,OAAO,YAAY,YAAa,SAAQ,KAAK,2BAA2B,GAAG;AAAA,IACjF;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,MAAM,OAA4C;AACtD,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB;AAAA;AAAA,EAGA,MAAM,QAAQ,IAAwC;AACpD,UAAM,EAAE,MAAM,MAAM,GAAG,KAAK,IAAI;AAChC,WAAO,KAAK,KAAK,EAAE,MAAM,MAAM,QAAQ,OAAO,GAAG,KAAK,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,SAET;AACA,UAAM,YAAY,OAAO;AACzB,WAAO;AAAA,MACL,QAAQ,CAAC,UAAkC;AACzC,cAAM,SAAuB,EAAE,GAAG,SAAS,GAAI,SAAS,CAAC,EAAG;AAC5D,cAAM,EAAE,MAAM,MAAM,GAAG,KAAK,IAAI;AAChC,eAAO,KAAK,KAAK;AAAA,UACf;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,OAAO;AAAA,UACP,KAAK,OAAO;AAAA,UACZ,GAAG;AAAA,QACL,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,MAAoC;AACxD,UAAM,EAAE,WAAW,iBAAiB,WAAW,GAAG,KAAK,IAAI;AAK3D,UAAM,KAAK,aAAa,KAAK;AAC7B,UAAM,OAAO,mBAAmB,KAAK;AACrC,UAAM,UAAU,aAAa,KAAK;AAClC,QAAI,OAAO,UAAa,SAAS,UAAa,YAAY,QAAW;AACnE,aAAO;AAAA,IACT;AAEA,UAAM,aAAsC,EAAE,GAAI,KAAK,cAAc,CAAC,EAAG;AACzE,QAAI,OAAO,UAAa,WAAW,eAAe,MAAM,QAAW;AACjE,iBAAW,eAAe,IAAI,OAAO,EAAE;AAAA,IACzC;AACA,QAAI,SAAS,UAAa,WAAW,qBAAqB,MAAM,QAAW;AACzE,iBAAW,qBAAqB,IAC9B,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,IAAI;AAAA,IACzD;AACA,QAAI,YAAY,UAAa,WAAW,cAAc,MAAM,QAAW;AACrE,iBAAW,cAAc,IAAI,OAAO,OAAO;AAAA,IAC7C;AACA,WAAO,EAAE,GAAG,MAAM,WAAW;AAAA,EAC/B;AAAA;AAAA,EAGA,MAAc,KAAK,MAA2C;AAC5D,QAAI,CAAC,KAAK,QAAS,QAAO,EAAE,IAAI,KAAK;AAErC,WAAO,KAAK,cAAc,IAAI;AAG9B,UAAM,UACJ,KAAK,WAAY,KAAa,YAAY,SACtC,EAAE,GAAG,MAAM,SAAS,KAAK,QAAQ,IACjC;AACN,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,aAAa;AAAA,QAClD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,MAAM;AAAA,QACtC;AAAA,QACA,MAAM,KAAK,UAAU,OAAO;AAAA;AAAA,QAE5B,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,cAAM,SAAS,EAAE,IAAI,OAAO,OAAO,QAAQ,IAAI,MAAM,GAAG,OAAO,KAAK,IAAI,KAAK,EAAE,GAAG;AAClF,aAAK,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACpC,eAAO;AAAA,MACT;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,aAAO,EAAE,IAAI,MAAM,UAAU,KAAK,UAAU,OAAO,KAAK,MAAM;AAAA,IAChE,SAAS,KAAK;AAEZ,WAAK,QAAQ,GAAG;AAChB,aAAO,EAAE,IAAI,OAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,IAC9E;AAAA,EACF;AACF;AAIA,SAAS,WAAW,UAAiC;AACnD,MAAI;AACF,WAAO,IAAI,IAAI,QAAQ,EAAE;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,SAAiB;AACxB,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAEA,IAAO,kBAAQ;","names":[]}
package/dist/browser.d.ts CHANGED
@@ -75,17 +75,17 @@ interface NeatlogsSpan {
75
75
  * (overrides the client default). One end-user per trace; the backend rolls it
76
76
  * up to the trace and its session. Ignored on child spans.
77
77
  */
78
- endUser?: string;
78
+ endUserId?: string;
79
79
  /** Arbitrary end-user fields for the trace root (overrides the client default). */
80
80
  endUserMetadata?: Record<string, unknown>;
81
81
  /**
82
82
  * SESSION this trace belongs to — the conversation/thread grouping many turns.
83
83
  * Only meaningful on the ROOT of a trace (overrides the client default). Every
84
- * trace sharing a `session` is grouped into one session in the dashboard; a new
85
- * trace per turn, all with the same `session`, forms a multi-turn conversation.
84
+ * trace sharing a `sessionId` is grouped into one session in the dashboard; a new
85
+ * trace per turn, all with the same `sessionId`, forms a multi-turn conversation.
86
86
  * Ignored on child spans.
87
87
  */
88
- session?: string;
88
+ sessionId?: string;
89
89
  }
90
90
  /** The root of a trace = a span node (its `name` becomes the workflow name). */
91
91
  type NeatlogsTrace = NeatlogsSpan;
@@ -111,18 +111,18 @@ interface NeatlogsOptions {
111
111
  /**
112
112
  * Default END-USER identity for every trace this client sends — the user of
113
113
  * your app, not the operator. One end-user per trace; the backend rolls it up
114
- * to the trace and its session. A per-call `endUser` (on trace()/trackAI())
115
- * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUser })`.
114
+ * to the trace and its session. A per-call `endUserId` (on trace()/trackAI())
115
+ * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUserId })`.
116
116
  */
117
- endUser?: string;
117
+ endUserId?: string;
118
118
  /** Default arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). */
119
119
  endUserMetadata?: Record<string, unknown>;
120
120
  /**
121
121
  * Default SESSION for every trace this client sends — the conversation/thread
122
- * these traces belong to. A per-call `session` (on trace()/trackAI()) overrides
123
- * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, session: convId })`.
122
+ * these traces belong to. A per-call `sessionId` (on trace()/trackAI()) overrides
123
+ * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, sessionId: convId })`.
124
124
  */
125
- session?: string;
125
+ sessionId?: string;
126
126
  }
127
127
  interface TrackResult {
128
128
  ok: boolean;
@@ -148,11 +148,11 @@ interface TrackAIInput {
148
148
  /** Override the inferred kind (defaults to LLM for trackAI). */
149
149
  kind?: NeatlogsKind | string;
150
150
  /** END-USER for this trace (overrides the client default). One per trace. */
151
- endUser?: string;
151
+ endUserId?: string;
152
152
  /** Arbitrary end-user fields for this trace (overrides the client default). */
153
153
  endUserMetadata?: Record<string, unknown>;
154
154
  /** SESSION this trace belongs to (overrides the client default). */
155
- session?: string;
155
+ sessionId?: string;
156
156
  }
157
157
  declare class Neatlogs {
158
158
  private readonly apiKey;
@@ -160,9 +160,9 @@ declare class Neatlogs {
160
160
  private readonly baseUrl;
161
161
  private readonly enabled;
162
162
  private readonly onError;
163
- private readonly endUser?;
163
+ private readonly endUserId?;
164
164
  private readonly endUserMetadata?;
165
- private readonly session?;
165
+ private readonly sessionId?;
166
166
  constructor(opts: NeatlogsOptions);
167
167
  /** Send a full (optionally nested) trace. Returns the backend's result. */
168
168
  trace(trace: NeatlogsTrace): Promise<TrackResult>;
@@ -178,7 +178,7 @@ declare class Neatlogs {
178
178
  };
179
179
  /**
180
180
  * Fold identity (end-user + session) onto the trace ROOT as canonical
181
- * attributes, and strip the convenience `endUser`/`endUserMetadata`/`session`
181
+ * attributes, and strip the convenience `endUserId`/`endUserMetadata`/`sessionId`
182
182
  * fields so they aren't sent as raw root fields. Per-call values win over the
183
183
  * client defaults; explicit `attributes` win over both. Only the root carries
184
184
  * identity — one end-user per trace, one session per trace.
package/dist/browser.mjs CHANGED
@@ -9,9 +9,9 @@ var Neatlogs = class {
9
9
  baseUrl;
10
10
  enabled;
11
11
  onError;
12
- endUser;
12
+ endUserId;
13
13
  endUserMetadata;
14
- session;
14
+ sessionId;
15
15
  constructor(opts) {
16
16
  if (!opts || !opts.apiKey) {
17
17
  throw new Error("Neatlogs: apiKey is required");
@@ -21,9 +21,9 @@ var Neatlogs = class {
21
21
  const endpoint = opts.endpoint || DEFAULT_ENDPOINT;
22
22
  this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;
23
23
  this.enabled = opts.enabled !== false;
24
- this.endUser = opts.endUser;
24
+ this.endUserId = opts.endUserId;
25
25
  this.endUserMetadata = opts.endUserMetadata;
26
- this.session = opts.session;
26
+ this.sessionId = opts.sessionId;
27
27
  this.onError = opts.onError || ((err) => {
28
28
  if (typeof console !== "undefined") console.warn("[neatlogs] send failed:", err);
29
29
  });
@@ -60,17 +60,17 @@ var Neatlogs = class {
60
60
  }
61
61
  /**
62
62
  * Fold identity (end-user + session) onto the trace ROOT as canonical
63
- * attributes, and strip the convenience `endUser`/`endUserMetadata`/`session`
63
+ * attributes, and strip the convenience `endUserId`/`endUserMetadata`/`sessionId`
64
64
  * fields so they aren't sent as raw root fields. Per-call values win over the
65
65
  * client defaults; explicit `attributes` win over both. Only the root carries
66
66
  * identity — one end-user per trace, one session per trace.
67
67
  */
68
68
  applyIdentity(body) {
69
- const { endUser, endUserMetadata, session, ...rest } = body;
70
- const id = endUser ?? this.endUser;
69
+ const { endUserId, endUserMetadata, sessionId, ...rest } = body;
70
+ const id = endUserId ?? this.endUserId;
71
71
  const meta = endUserMetadata ?? this.endUserMetadata;
72
- const sessionId = session ?? this.session;
73
- if (id === void 0 && meta === void 0 && sessionId === void 0) {
72
+ const session = sessionId ?? this.sessionId;
73
+ if (id === void 0 && meta === void 0 && session === void 0) {
74
74
  return rest;
75
75
  }
76
76
  const attributes = { ...rest.attributes ?? {} };
@@ -80,8 +80,8 @@ var Neatlogs = class {
80
80
  if (meta !== void 0 && attributes[END_USER_METADATA_KEY] === void 0) {
81
81
  attributes[END_USER_METADATA_KEY] = typeof meta === "string" ? meta : JSON.stringify(meta);
82
82
  }
83
- if (sessionId !== void 0 && attributes[SESSION_ID_KEY] === void 0) {
84
- attributes[SESSION_ID_KEY] = String(sessionId);
83
+ if (session !== void 0 && attributes[SESSION_ID_KEY] === void 0) {
84
+ attributes[SESSION_ID_KEY] = String(session);
85
85
  }
86
86
  return { ...rest, attributes };
87
87
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Neatlogs Browser SDK — `neatlogs/browser`\n *\n * A minimal, browser-safe client for sending traces to Neatlogs from web apps.\n * It has ZERO dependencies (no OpenTelemetry, no Node APIs) — only `fetch` — so\n * it bundles cleanly into front-end apps. It POSTs plain JSON to the backend's\n * simple trace endpoint (`/v1/trace`); the backend generates trace/span ids,\n * builds the hierarchy from nesting, infers cost from model+tokens, and pushes\n * through the normal pipeline. Nothing here streams OTLP.\n *\n * Usage:\n * import { Neatlogs } from 'neatlogs/browser';\n * const nl = new Neatlogs({ apiKey: 'nl_...' });\n *\n * // one-shot AI interaction\n * await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output,\n * tokens: { prompt: 10, completion: 5 } });\n *\n * // a full nested trace (same shape the backend's POST /v1/trace accepts)\n * await nl.trace({ name: 'support-chat', children: [\n * { name: 'retrieve', query, documents },\n * { name: 'answer', model: 'gpt-4o', input, output },\n * ]});\n *\n * // streaming: open, accumulate, finish\n * const t = nl.startTrace({ name: 'chat', model: 'gpt-4o', input });\n * t.finish({ output: full, tokens: { prompt, completion } });\n */\n\nconst DEFAULT_ENDPOINT = \"https://ingest.neatlogs.com\";\n\n// Canonical identity attribute keys (inlined — this file stays dependency-free).\nconst END_USER_ID_KEY = \"neatlogs.end_user.id\";\nconst END_USER_METADATA_KEY = \"neatlogs.end_user.metadata\";\nconst SESSION_ID_KEY = \"neatlogs.session.id\";\n\n// --- the simple trace shape the backend (/v1/trace) accepts --------------------\n// Mirrors the server-side SimpleSpan; kept local so this file has no imports.\n\nexport interface NeatlogsLog {\n level?: string;\n message: string;\n timestamp?: string;\n}\n\n/**\n * Span kinds the backend accepts (the canonical set). `kind` is optional — when\n * omitted the backend infers it from the fields present.\n */\nexport type NeatlogsKind =\n | \"WORKFLOW\" | \"AGENT\" | \"CHAIN\" | \"TOOL\" | \"RETRIEVER\" | \"RERANKER\"\n | \"EMBEDDING\" | \"LLM\" | \"GUARDRAIL\" | \"MCP_TOOL\" | \"TASK\"\n | \"VECTOR_STORE\" | \"EVALUATOR\" | \"HTTP\";\n\nexport interface NeatlogsSpan {\n name: string;\n /** Optional — the backend infers the kind from fields when omitted. */\n kind?: NeatlogsKind | string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n query?: unknown;\n documents?: unknown;\n tool_name?: string;\n passed?: boolean;\n score?: number;\n metadata?: Record<string, unknown>;\n status?: string;\n error?: string;\n start?: string;\n end?: string;\n /** Simplest way to record latency — the backend derives end from start + this. */\n duration_ms?: number;\n /**\n * Full canonical-attribute escape hatch. Send ANY neatlogs.* attribute the SDK\n * supports — e.g. { \"neatlogs.llm.temperature\": 0.7, \"neatlogs.agent.role\":\n * \"researcher\", \"neatlogs.tool.parameters\": {...} }. Non-canonical keys are\n * dropped server-side. The fields above (model/tokens/query/...) are shortcuts\n * for the common ones; explicit `attributes` win on conflict.\n */\n attributes?: Record<string, unknown>;\n children?: NeatlogsSpan[];\n logs?: NeatlogsLog[];\n /**\n * END-USER this trace belongs to — only meaningful on the ROOT of a trace\n * (overrides the client default). One end-user per trace; the backend rolls it\n * up to the trace and its session. Ignored on child spans.\n */\n endUser?: string;\n /** Arbitrary end-user fields for the trace root (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * SESSION this trace belongs to — the conversation/thread grouping many turns.\n * Only meaningful on the ROOT of a trace (overrides the client default). Every\n * trace sharing a `session` is grouped into one session in the dashboard; a new\n * trace per turn, all with the same `session`, forms a multi-turn conversation.\n * Ignored on child spans.\n */\n session?: string;\n}\n\n/** The root of a trace = a span node (its `name` becomes the workflow name). */\nexport type NeatlogsTrace = NeatlogsSpan;\n\nexport interface NeatlogsOptions {\n /**\n * Your Neatlogs WRITE key (`nlw_…`) — an ingest-only credential safe to embed in\n * browser code. (A full project key also works but should not be exposed client-side,\n * since it can read data.)\n */\n apiKey: string;\n /**\n * Project NAME to ingest into. REQUIRED when using a write key (the key identifies\n * you, not a project). Sent as the root `project` field on every trace. Ignored for\n * a full project key (already project-scoped).\n */\n project?: string;\n /** Backend base URL. Defaults to the same host the SDKs use. */\n endpoint?: string;\n /** Set false to validate calls without sending (default true). */\n enabled?: boolean;\n /** Called on transport errors instead of throwing (default: console.warn). */\n onError?: (err: unknown) => void;\n /**\n * Default END-USER identity for every trace this client sends — the user of\n * your app, not the operator. One end-user per trace; the backend rolls it up\n * to the trace and its session. A per-call `endUser` (on trace()/trackAI())\n * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUser })`.\n */\n endUser?: string;\n /** Default arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * Default SESSION for every trace this client sends — the conversation/thread\n * these traces belong to. A per-call `session` (on trace()/trackAI()) overrides\n * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, session: convId })`.\n */\n session?: string;\n}\n\nexport interface TrackResult {\n ok: boolean;\n trace_id?: string;\n spans?: number;\n error?: string;\n}\n\n/** Shorthand for a single AI interaction → a one-span trace. */\nexport interface TrackAIInput {\n name: string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n metadata?: Record<string, unknown>;\n duration_ms?: number;\n /** Any canonical neatlogs.* attributes (see NeatlogsSpan.attributes). */\n attributes?: Record<string, unknown>;\n /** Override the inferred kind (defaults to LLM for trackAI). */\n kind?: NeatlogsKind | string;\n /** END-USER for this trace (overrides the client default). One per trace. */\n endUser?: string;\n /** Arbitrary end-user fields for this trace (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /** SESSION this trace belongs to (overrides the client default). */\n session?: string;\n}\n\nexport class Neatlogs {\n private readonly apiKey: string;\n private readonly project?: string;\n private readonly baseUrl: string;\n private readonly enabled: boolean;\n private readonly onError: (err: unknown) => void;\n private readonly endUser?: string;\n private readonly endUserMetadata?: Record<string, unknown>;\n private readonly session?: string;\n\n constructor(opts: NeatlogsOptions) {\n if (!opts || !opts.apiKey) {\n throw new Error(\"Neatlogs: apiKey is required\");\n }\n this.apiKey = opts.apiKey;\n this.project = opts.project;\n // Use the origin of the configured endpoint (same convention as the Node SDK),\n // so passing a full /v1/traces URL or a bare host both work.\n const endpoint = opts.endpoint || DEFAULT_ENDPOINT;\n this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;\n this.enabled = opts.enabled !== false;\n this.endUser = opts.endUser;\n this.endUserMetadata = opts.endUserMetadata;\n this.session = opts.session;\n this.onError =\n opts.onError ||\n ((err) => {\n // eslint-disable-next-line no-console\n if (typeof console !== \"undefined\") console.warn(\"[neatlogs] send failed:\", err);\n });\n }\n\n /** Send a full (optionally nested) trace. Returns the backend's result. */\n async trace(trace: NeatlogsTrace): Promise<TrackResult> {\n return this.post(trace);\n }\n\n /** Send a single AI interaction as a one-span trace (kind defaults to LLM). */\n async trackAI(ai: TrackAIInput): Promise<TrackResult> {\n const { name, kind, ...rest } = ai;\n return this.post({ name, kind: kind ?? \"LLM\", ...rest });\n }\n\n /**\n * Begin a trace you'll complete later (e.g. streaming). Buffers the partial\n * input; call `.finish()` with the final output/tokens to send it. Nothing is\n * sent until `finish()`.\n */\n startTrace(initial: TrackAIInput): {\n finish: (final?: Partial<TrackAIInput>) => Promise<TrackResult>;\n } {\n const startedAt = nowIso();\n return {\n finish: (final?: Partial<TrackAIInput>) => {\n const merged: TrackAIInput = { ...initial, ...(final ?? {}) };\n const { name, kind, ...rest } = merged;\n return this.post({\n name,\n kind: kind ?? \"LLM\",\n start: startedAt,\n end: nowIso(),\n ...rest,\n });\n },\n };\n }\n\n /**\n * Fold identity (end-user + session) onto the trace ROOT as canonical\n * attributes, and strip the convenience `endUser`/`endUserMetadata`/`session`\n * fields so they aren't sent as raw root fields. Per-call values win over the\n * client defaults; explicit `attributes` win over both. Only the root carries\n * identity — one end-user per trace, one session per trace.\n */\n private applyIdentity(body: NeatlogsTrace): NeatlogsTrace {\n const { endUser, endUserMetadata, session, ...rest } = body as NeatlogsTrace & {\n endUser?: string;\n endUserMetadata?: Record<string, unknown>;\n session?: string;\n };\n const id = endUser ?? this.endUser;\n const meta = endUserMetadata ?? this.endUserMetadata;\n const sessionId = session ?? this.session;\n if (id === undefined && meta === undefined && sessionId === undefined) {\n return rest;\n }\n\n const attributes: Record<string, unknown> = { ...(rest.attributes ?? {}) };\n if (id !== undefined && attributes[END_USER_ID_KEY] === undefined) {\n attributes[END_USER_ID_KEY] = String(id);\n }\n if (meta !== undefined && attributes[END_USER_METADATA_KEY] === undefined) {\n attributes[END_USER_METADATA_KEY] =\n typeof meta === \"string\" ? meta : JSON.stringify(meta);\n }\n if (sessionId !== undefined && attributes[SESSION_ID_KEY] === undefined) {\n attributes[SESSION_ID_KEY] = String(sessionId);\n }\n return { ...rest, attributes };\n }\n\n /** POST the trace JSON to the backend's /v1/trace endpoint. */\n private async post(body: NeatlogsTrace): Promise<TrackResult> {\n if (!this.enabled) return { ok: true };\n // Fold identity (end-user + session) onto the root before anything else.\n body = this.applyIdentity(body);\n // Inject the configured project name into the root (required for write keys;\n // ignored server-side for full project keys). A `project` already on the body wins.\n const payload =\n this.project && (body as any).project === undefined\n ? { ...body, project: this.project }\n : body;\n try {\n const res = await fetch(`${this.baseUrl}/v1/trace`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(payload),\n // keepalive lets the request survive a page unload (e.g. on navigation).\n keepalive: true,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n const result = { ok: false, error: `HTTP ${res.status}${text ? `: ${text}` : \"\"}` };\n this.onError(new Error(result.error));\n return result;\n }\n const data = (await res.json().catch(() => ({}))) as Partial<TrackResult>;\n return { ok: true, trace_id: data.trace_id, spans: data.spans };\n } catch (err) {\n // Never throw into the host app over telemetry.\n this.onError(err);\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n}\n\n// --- helpers (kept local, no imports) -----------------------------------------\n\nfunction safeOrigin(endpoint: string): string | null {\n try {\n return new URL(endpoint).origin;\n } catch {\n return null;\n }\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\nexport default Neatlogs;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["/**\n * Neatlogs Browser SDK — `neatlogs/browser`\n *\n * A minimal, browser-safe client for sending traces to Neatlogs from web apps.\n * It has ZERO dependencies (no OpenTelemetry, no Node APIs) — only `fetch` — so\n * it bundles cleanly into front-end apps. It POSTs plain JSON to the backend's\n * simple trace endpoint (`/v1/trace`); the backend generates trace/span ids,\n * builds the hierarchy from nesting, infers cost from model+tokens, and pushes\n * through the normal pipeline. Nothing here streams OTLP.\n *\n * Usage:\n * import { Neatlogs } from 'neatlogs/browser';\n * const nl = new Neatlogs({ apiKey: 'nl_...' });\n *\n * // one-shot AI interaction\n * await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output,\n * tokens: { prompt: 10, completion: 5 } });\n *\n * // a full nested trace (same shape the backend's POST /v1/trace accepts)\n * await nl.trace({ name: 'support-chat', children: [\n * { name: 'retrieve', query, documents },\n * { name: 'answer', model: 'gpt-4o', input, output },\n * ]});\n *\n * // streaming: open, accumulate, finish\n * const t = nl.startTrace({ name: 'chat', model: 'gpt-4o', input });\n * t.finish({ output: full, tokens: { prompt, completion } });\n */\n\nconst DEFAULT_ENDPOINT = \"https://ingest.neatlogs.com\";\n\n// Canonical identity attribute keys (inlined — this file stays dependency-free).\nconst END_USER_ID_KEY = \"neatlogs.end_user.id\";\nconst END_USER_METADATA_KEY = \"neatlogs.end_user.metadata\";\nconst SESSION_ID_KEY = \"neatlogs.session.id\";\n\n// --- the simple trace shape the backend (/v1/trace) accepts --------------------\n// Mirrors the server-side SimpleSpan; kept local so this file has no imports.\n\nexport interface NeatlogsLog {\n level?: string;\n message: string;\n timestamp?: string;\n}\n\n/**\n * Span kinds the backend accepts (the canonical set). `kind` is optional — when\n * omitted the backend infers it from the fields present.\n */\nexport type NeatlogsKind =\n | \"WORKFLOW\" | \"AGENT\" | \"CHAIN\" | \"TOOL\" | \"RETRIEVER\" | \"RERANKER\"\n | \"EMBEDDING\" | \"LLM\" | \"GUARDRAIL\" | \"MCP_TOOL\" | \"TASK\"\n | \"VECTOR_STORE\" | \"EVALUATOR\" | \"HTTP\";\n\nexport interface NeatlogsSpan {\n name: string;\n /** Optional — the backend infers the kind from fields when omitted. */\n kind?: NeatlogsKind | string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n query?: unknown;\n documents?: unknown;\n tool_name?: string;\n passed?: boolean;\n score?: number;\n metadata?: Record<string, unknown>;\n status?: string;\n error?: string;\n start?: string;\n end?: string;\n /** Simplest way to record latency — the backend derives end from start + this. */\n duration_ms?: number;\n /**\n * Full canonical-attribute escape hatch. Send ANY neatlogs.* attribute the SDK\n * supports — e.g. { \"neatlogs.llm.temperature\": 0.7, \"neatlogs.agent.role\":\n * \"researcher\", \"neatlogs.tool.parameters\": {...} }. Non-canonical keys are\n * dropped server-side. The fields above (model/tokens/query/...) are shortcuts\n * for the common ones; explicit `attributes` win on conflict.\n */\n attributes?: Record<string, unknown>;\n children?: NeatlogsSpan[];\n logs?: NeatlogsLog[];\n /**\n * END-USER this trace belongs to — only meaningful on the ROOT of a trace\n * (overrides the client default). One end-user per trace; the backend rolls it\n * up to the trace and its session. Ignored on child spans.\n */\n endUserId?: string;\n /** Arbitrary end-user fields for the trace root (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * SESSION this trace belongs to — the conversation/thread grouping many turns.\n * Only meaningful on the ROOT of a trace (overrides the client default). Every\n * trace sharing a `sessionId` is grouped into one session in the dashboard; a new\n * trace per turn, all with the same `sessionId`, forms a multi-turn conversation.\n * Ignored on child spans.\n */\n sessionId?: string;\n}\n\n/** The root of a trace = a span node (its `name` becomes the workflow name). */\nexport type NeatlogsTrace = NeatlogsSpan;\n\nexport interface NeatlogsOptions {\n /**\n * Your Neatlogs WRITE key (`nlw_…`) — an ingest-only credential safe to embed in\n * browser code. (A full project key also works but should not be exposed client-side,\n * since it can read data.)\n */\n apiKey: string;\n /**\n * Project NAME to ingest into. REQUIRED when using a write key (the key identifies\n * you, not a project). Sent as the root `project` field on every trace. Ignored for\n * a full project key (already project-scoped).\n */\n project?: string;\n /** Backend base URL. Defaults to the same host the SDKs use. */\n endpoint?: string;\n /** Set false to validate calls without sending (default true). */\n enabled?: boolean;\n /** Called on transport errors instead of throwing (default: console.warn). */\n onError?: (err: unknown) => void;\n /**\n * Default END-USER identity for every trace this client sends — the user of\n * your app, not the operator. One end-user per trace; the backend rolls it up\n * to the trace and its session. A per-call `endUserId` (on trace()/trackAI())\n * overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey, endUserId })`.\n */\n endUserId?: string;\n /** Default arbitrary end-user fields stored as JSON (e.g. { plan: 'pro' }). */\n endUserMetadata?: Record<string, unknown>;\n /**\n * Default SESSION for every trace this client sends — the conversation/thread\n * these traces belong to. A per-call `sessionId` (on trace()/trackAI()) overrides\n * this. Set it once per conversation, e.g. `new Neatlogs({ apiKey, sessionId: convId })`.\n */\n sessionId?: string;\n}\n\nexport interface TrackResult {\n ok: boolean;\n trace_id?: string;\n spans?: number;\n error?: string;\n}\n\n/** Shorthand for a single AI interaction → a one-span trace. */\nexport interface TrackAIInput {\n name: string;\n input?: unknown;\n output?: unknown;\n model?: string;\n tokens?: { prompt?: number; completion?: number; total?: number };\n metadata?: Record<string, unknown>;\n duration_ms?: number;\n /** Any canonical neatlogs.* attributes (see NeatlogsSpan.attributes). */\n attributes?: Record<string, unknown>;\n /** Override the inferred kind (defaults to LLM for trackAI). */\n kind?: NeatlogsKind | string;\n /** END-USER for this trace (overrides the client default). One per trace. */\n endUserId?: string;\n /** Arbitrary end-user fields for this trace (overrides the client default). */\n endUserMetadata?: Record<string, unknown>;\n /** SESSION this trace belongs to (overrides the client default). */\n sessionId?: string;\n}\n\nexport class Neatlogs {\n private readonly apiKey: string;\n private readonly project?: string;\n private readonly baseUrl: string;\n private readonly enabled: boolean;\n private readonly onError: (err: unknown) => void;\n private readonly endUserId?: string;\n private readonly endUserMetadata?: Record<string, unknown>;\n private readonly sessionId?: string;\n\n constructor(opts: NeatlogsOptions) {\n if (!opts || !opts.apiKey) {\n throw new Error(\"Neatlogs: apiKey is required\");\n }\n this.apiKey = opts.apiKey;\n this.project = opts.project;\n // Use the origin of the configured endpoint (same convention as the Node SDK),\n // so passing a full /v1/traces URL or a bare host both work.\n const endpoint = opts.endpoint || DEFAULT_ENDPOINT;\n this.baseUrl = safeOrigin(endpoint) || DEFAULT_ENDPOINT;\n this.enabled = opts.enabled !== false;\n this.endUserId = opts.endUserId;\n this.endUserMetadata = opts.endUserMetadata;\n this.sessionId = opts.sessionId;\n this.onError =\n opts.onError ||\n ((err) => {\n // eslint-disable-next-line no-console\n if (typeof console !== \"undefined\") console.warn(\"[neatlogs] send failed:\", err);\n });\n }\n\n /** Send a full (optionally nested) trace. Returns the backend's result. */\n async trace(trace: NeatlogsTrace): Promise<TrackResult> {\n return this.post(trace);\n }\n\n /** Send a single AI interaction as a one-span trace (kind defaults to LLM). */\n async trackAI(ai: TrackAIInput): Promise<TrackResult> {\n const { name, kind, ...rest } = ai;\n return this.post({ name, kind: kind ?? \"LLM\", ...rest });\n }\n\n /**\n * Begin a trace you'll complete later (e.g. streaming). Buffers the partial\n * input; call `.finish()` with the final output/tokens to send it. Nothing is\n * sent until `finish()`.\n */\n startTrace(initial: TrackAIInput): {\n finish: (final?: Partial<TrackAIInput>) => Promise<TrackResult>;\n } {\n const startedAt = nowIso();\n return {\n finish: (final?: Partial<TrackAIInput>) => {\n const merged: TrackAIInput = { ...initial, ...(final ?? {}) };\n const { name, kind, ...rest } = merged;\n return this.post({\n name,\n kind: kind ?? \"LLM\",\n start: startedAt,\n end: nowIso(),\n ...rest,\n });\n },\n };\n }\n\n /**\n * Fold identity (end-user + session) onto the trace ROOT as canonical\n * attributes, and strip the convenience `endUserId`/`endUserMetadata`/`sessionId`\n * fields so they aren't sent as raw root fields. Per-call values win over the\n * client defaults; explicit `attributes` win over both. Only the root carries\n * identity — one end-user per trace, one session per trace.\n */\n private applyIdentity(body: NeatlogsTrace): NeatlogsTrace {\n const { endUserId, endUserMetadata, sessionId, ...rest } = body as NeatlogsTrace & {\n endUserId?: string;\n endUserMetadata?: Record<string, unknown>;\n sessionId?: string;\n };\n const id = endUserId ?? this.endUserId;\n const meta = endUserMetadata ?? this.endUserMetadata;\n const session = sessionId ?? this.sessionId;\n if (id === undefined && meta === undefined && session === undefined) {\n return rest;\n }\n\n const attributes: Record<string, unknown> = { ...(rest.attributes ?? {}) };\n if (id !== undefined && attributes[END_USER_ID_KEY] === undefined) {\n attributes[END_USER_ID_KEY] = String(id);\n }\n if (meta !== undefined && attributes[END_USER_METADATA_KEY] === undefined) {\n attributes[END_USER_METADATA_KEY] =\n typeof meta === \"string\" ? meta : JSON.stringify(meta);\n }\n if (session !== undefined && attributes[SESSION_ID_KEY] === undefined) {\n attributes[SESSION_ID_KEY] = String(session);\n }\n return { ...rest, attributes };\n }\n\n /** POST the trace JSON to the backend's /v1/trace endpoint. */\n private async post(body: NeatlogsTrace): Promise<TrackResult> {\n if (!this.enabled) return { ok: true };\n // Fold identity (end-user + session) onto the root before anything else.\n body = this.applyIdentity(body);\n // Inject the configured project name into the root (required for write keys;\n // ignored server-side for full project keys). A `project` already on the body wins.\n const payload =\n this.project && (body as any).project === undefined\n ? { ...body, project: this.project }\n : body;\n try {\n const res = await fetch(`${this.baseUrl}/v1/trace`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(payload),\n // keepalive lets the request survive a page unload (e.g. on navigation).\n keepalive: true,\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n const result = { ok: false, error: `HTTP ${res.status}${text ? `: ${text}` : \"\"}` };\n this.onError(new Error(result.error));\n return result;\n }\n const data = (await res.json().catch(() => ({}))) as Partial<TrackResult>;\n return { ok: true, trace_id: data.trace_id, spans: data.spans };\n } catch (err) {\n // Never throw into the host app over telemetry.\n this.onError(err);\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n}\n\n// --- helpers (kept local, no imports) -----------------------------------------\n\nfunction safeOrigin(endpoint: string): string | null {\n try {\n return new URL(endpoint).origin;\n } catch {\n return null;\n }\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\nexport default Neatlogs;\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -2217,9 +2217,84 @@ function applyMask(spanData, globalMask) {
2217
2217
  }
2218
2218
  }
2219
2219
 
2220
- // src/prompt/template.ts
2220
+ // src/core/identity.ts
2221
2221
  var import_node_async_hooks = require("async_hooks");
2222
- var _promptStorage = new import_node_async_hooks.AsyncLocalStorage();
2222
+ var _GLOBAL_KEY = /* @__PURE__ */ Symbol.for("neatlogs.identity.storage");
2223
+ var _g = globalThis;
2224
+ var _storage = _g[_GLOBAL_KEY] ?? (_g[_GLOBAL_KEY] = new import_node_async_hooks.AsyncLocalStorage());
2225
+ function currentSessionId() {
2226
+ return _storage.getStore()?.sessionId;
2227
+ }
2228
+ function currentEndUserId() {
2229
+ return _storage.getStore()?.endUserId;
2230
+ }
2231
+ function currentEndUserMetadata() {
2232
+ return _storage.getStore()?.endUserMetadata;
2233
+ }
2234
+ function identify(opts, fn) {
2235
+ const prev = _storage.getStore();
2236
+ const next = { ...prev ?? {} };
2237
+ if (opts.sessionId !== void 0) next.sessionId = opts.sessionId;
2238
+ if (opts.endUserId !== void 0) next.endUserId = opts.endUserId;
2239
+ if (opts.endUserMetadata !== void 0) next.endUserMetadata = opts.endUserMetadata;
2240
+ return _storage.run(next, fn);
2241
+ }
2242
+
2243
+ // src/core/session.ts
2244
+ var logger4 = getLogger();
2245
+ var SESSION_ID_KEY = "neatlogs.session.id";
2246
+ function applySessionAttributes(span2, sessionId, isRoot = true) {
2247
+ const resolved = isRoot ? sessionId ?? currentSessionId() : sessionId;
2248
+ if (!resolved) return;
2249
+ if (!isRoot) {
2250
+ logger4.debug(
2251
+ "[session] Ignoring sessionId on a non-root span \u2014 declare it on the trace root (top-level trace()/span()) or identify()."
2252
+ );
2253
+ return;
2254
+ }
2255
+ span2.setAttribute(SESSION_ID_KEY, String(resolved));
2256
+ }
2257
+
2258
+ // src/core/end-user.ts
2259
+ var import_api = require("@opentelemetry/api");
2260
+ var logger5 = getLogger();
2261
+ var END_USER_ID_KEY = "neatlogs.end_user.id";
2262
+ var END_USER_METADATA_KEY = "neatlogs.end_user.metadata";
2263
+ function normalizeEndUserMetadata(metadata) {
2264
+ if (metadata === void 0 || metadata === null) return void 0;
2265
+ if (typeof metadata === "string") return metadata || void 0;
2266
+ try {
2267
+ return JSON.stringify(metadata);
2268
+ } catch {
2269
+ return JSON.stringify(String(metadata));
2270
+ }
2271
+ }
2272
+ function isRootSpan() {
2273
+ const current = import_api.trace.getSpan(import_api.context.active());
2274
+ return !(current && current.isRecording());
2275
+ }
2276
+ function applyEndUserAttributes(span2, endUserId, endUserMetadata, isRoot = true) {
2277
+ const resolvedId = isRoot ? endUserId ?? currentEndUserId() : endUserId;
2278
+ const resolvedMeta = isRoot ? endUserMetadata ?? currentEndUserMetadata() : endUserMetadata;
2279
+ if (!resolvedId && !resolvedMeta) return;
2280
+ if (!isRoot) {
2281
+ logger5.debug(
2282
+ "[end_user] Ignoring endUserId/endUserMetadata on a non-root span \u2014 declare it on the trace root (top-level trace()/span()) or identify()."
2283
+ );
2284
+ return;
2285
+ }
2286
+ if (resolvedId) {
2287
+ span2.setAttribute(END_USER_ID_KEY, String(resolvedId));
2288
+ }
2289
+ const metaJson = normalizeEndUserMetadata(resolvedMeta);
2290
+ if (metaJson) {
2291
+ span2.setAttribute(END_USER_METADATA_KEY, metaJson);
2292
+ }
2293
+ }
2294
+
2295
+ // src/prompt/template.ts
2296
+ var import_node_async_hooks2 = require("async_hooks");
2297
+ var _promptStorage = new import_node_async_hooks2.AsyncLocalStorage();
2223
2298
  var PromptContext = class {
2224
2299
  /**
2225
2300
  * Store system prompt template and variables in context.
@@ -2246,7 +2321,7 @@ var PromptContext = class {
2246
2321
  _promptStorage.enterWith(void 0);
2247
2322
  }
2248
2323
  };
2249
- var _userPromptStorage = new import_node_async_hooks.AsyncLocalStorage();
2324
+ var _userPromptStorage = new import_node_async_hooks2.AsyncLocalStorage();
2250
2325
  var UserPromptContext = class {
2251
2326
  /**
2252
2327
  * Store user prompt template and variables in context.
@@ -2372,83 +2447,6 @@ var UserPromptTemplate = class extends BasePromptTemplate {
2372
2447
  // src/core/context.ts
2373
2448
  var import_api3 = require("@opentelemetry/api");
2374
2449
 
2375
- // src/core/end-user.ts
2376
- var import_api = require("@opentelemetry/api");
2377
-
2378
- // src/core/identity.ts
2379
- var import_node_async_hooks2 = require("async_hooks");
2380
- var _GLOBAL_KEY = /* @__PURE__ */ Symbol.for("neatlogs.identity.storage");
2381
- var _g = globalThis;
2382
- var _storage = _g[_GLOBAL_KEY] ?? (_g[_GLOBAL_KEY] = new import_node_async_hooks2.AsyncLocalStorage());
2383
- function currentSessionId() {
2384
- return _storage.getStore()?.sessionId;
2385
- }
2386
- function currentEndUserId() {
2387
- return _storage.getStore()?.endUserId;
2388
- }
2389
- function currentEndUserMetadata() {
2390
- return _storage.getStore()?.endUserMetadata;
2391
- }
2392
- function identify(opts, fn) {
2393
- const prev = _storage.getStore();
2394
- const next = { ...prev ?? {} };
2395
- if (opts.sessionId !== void 0) next.sessionId = opts.sessionId;
2396
- if (opts.endUserId !== void 0) next.endUserId = opts.endUserId;
2397
- if (opts.endUserMetadata !== void 0) next.endUserMetadata = opts.endUserMetadata;
2398
- return _storage.run(next, fn);
2399
- }
2400
-
2401
- // src/core/end-user.ts
2402
- var logger4 = getLogger();
2403
- var END_USER_ID_KEY = "neatlogs.end_user.id";
2404
- var END_USER_METADATA_KEY = "neatlogs.end_user.metadata";
2405
- function normalizeEndUserMetadata(metadata) {
2406
- if (metadata === void 0 || metadata === null) return void 0;
2407
- if (typeof metadata === "string") return metadata || void 0;
2408
- try {
2409
- return JSON.stringify(metadata);
2410
- } catch {
2411
- return JSON.stringify(String(metadata));
2412
- }
2413
- }
2414
- function isRootSpan() {
2415
- const current = import_api.trace.getSpan(import_api.context.active());
2416
- return !(current && current.isRecording());
2417
- }
2418
- function applyEndUserAttributes(span2, endUserId, endUserMetadata, isRoot = true) {
2419
- const resolvedId = isRoot ? endUserId ?? currentEndUserId() : endUserId;
2420
- const resolvedMeta = isRoot ? endUserMetadata ?? currentEndUserMetadata() : endUserMetadata;
2421
- if (!resolvedId && !resolvedMeta) return;
2422
- if (!isRoot) {
2423
- logger4.debug(
2424
- "[end_user] Ignoring endUserId/endUserMetadata on a non-root span \u2014 declare it on the trace root (top-level trace()/span()) or identify()."
2425
- );
2426
- return;
2427
- }
2428
- if (resolvedId) {
2429
- span2.setAttribute(END_USER_ID_KEY, String(resolvedId));
2430
- }
2431
- const metaJson = normalizeEndUserMetadata(resolvedMeta);
2432
- if (metaJson) {
2433
- span2.setAttribute(END_USER_METADATA_KEY, metaJson);
2434
- }
2435
- }
2436
-
2437
- // src/core/session.ts
2438
- var logger5 = getLogger();
2439
- var SESSION_ID_KEY = "neatlogs.session.id";
2440
- function applySessionAttributes(span2, sessionId, isRoot = true) {
2441
- const resolved = isRoot ? sessionId ?? currentSessionId() : sessionId;
2442
- if (!resolved) return;
2443
- if (!isRoot) {
2444
- logger5.debug(
2445
- "[session] Ignoring sessionId on a non-root span \u2014 declare it on the trace root (top-level trace()/span()) or identify()."
2446
- );
2447
- return;
2448
- }
2449
- span2.setAttribute(SESSION_ID_KEY, String(resolved));
2450
- }
2451
-
2452
2450
  // src/decorators/base.ts
2453
2451
  var import_api2 = require("@opentelemetry/api");
2454
2452
  var logger6 = getLogger();
@@ -4426,6 +4424,10 @@ var NeatlogsSpanProcessor = class {
4426
4424
  onStart(span2, parentContext) {
4427
4425
  const startTime = import_node_perf_hooks.performance.now();
4428
4426
  try {
4427
+ if (!span2.parentSpanId) {
4428
+ applySessionAttributes(span2, void 0, true);
4429
+ applyEndUserAttributes(span2, void 0, void 0, true);
4430
+ }
4429
4431
  const attrs = span2.attributes ?? {};
4430
4432
  const spanKind = attrs["openinference.span.kind"];
4431
4433
  const spanName = typeof span2.name === "string" ? span2.name : "";
@@ -5818,7 +5820,7 @@ async function removeTag(name, tag) {
5818
5820
  }
5819
5821
 
5820
5822
  // src/version.ts
5821
- var __version__ = "1.1.2";
5823
+ var __version__ = "1.1.4";
5822
5824
 
5823
5825
  // src/init.ts
5824
5826
  var logger13 = getLogger();