neatlogs 1.1.2 → 1.1.5
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 +11 -11
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +15 -15
- package/dist/browser.mjs +11 -11
- package/dist/browser.mjs.map +1 -1
- package/dist/google-genai.cjs +640 -0
- package/dist/google-genai.cjs.map +1 -0
- package/dist/google-genai.d.ts +42 -0
- package/dist/google-genai.mjs +622 -0
- package/dist/google-genai.mjs.map +1 -0
- package/dist/index.cjs +746 -346
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +745 -348
- package/dist/index.mjs.map +1 -1
- package/dist/opencode-plugin.cjs +1 -1
- package/dist/opencode-plugin.cjs.map +1 -1
- package/dist/opencode-plugin.mjs +1 -1
- package/dist/opencode-plugin.mjs.map +1 -1
- package/package.json +12 -1
package/dist/browser.cjs
CHANGED
|
@@ -34,9 +34,9 @@ var Neatlogs = class {
|
|
|
34
34
|
baseUrl;
|
|
35
35
|
enabled;
|
|
36
36
|
onError;
|
|
37
|
-
|
|
37
|
+
endUserId;
|
|
38
38
|
endUserMetadata;
|
|
39
|
-
|
|
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.
|
|
49
|
+
this.endUserId = opts.endUserId;
|
|
50
50
|
this.endUserMetadata = opts.endUserMetadata;
|
|
51
|
-
this.
|
|
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 `
|
|
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 {
|
|
95
|
-
const id =
|
|
94
|
+
const { endUserId, endUserMetadata, sessionId, ...rest } = body;
|
|
95
|
+
const id = endUserId ?? this.endUserId;
|
|
96
96
|
const meta = endUserMetadata ?? this.endUserMetadata;
|
|
97
|
-
const
|
|
98
|
-
if (id === void 0 && meta === 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 (
|
|
109
|
-
attributes[SESSION_ID_KEY] = String(
|
|
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
|
}
|
package/dist/browser.cjs.map
CHANGED
|
@@ -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/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
|
-
|
|
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 `
|
|
85
|
-
* trace per turn, all with the same `
|
|
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
|
-
|
|
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 `
|
|
115
|
-
* overrides this. Set it once after login, e.g. `new Neatlogs({ apiKey,
|
|
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
|
-
|
|
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 `
|
|
123
|
-
* this. Set it once per conversation, e.g. `new Neatlogs({ apiKey,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
163
|
+
private readonly endUserId?;
|
|
164
164
|
private readonly endUserMetadata?;
|
|
165
|
-
private readonly
|
|
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 `
|
|
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
|
-
|
|
12
|
+
endUserId;
|
|
13
13
|
endUserMetadata;
|
|
14
|
-
|
|
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.
|
|
24
|
+
this.endUserId = opts.endUserId;
|
|
25
25
|
this.endUserMetadata = opts.endUserMetadata;
|
|
26
|
-
this.
|
|
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 `
|
|
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 {
|
|
70
|
-
const id =
|
|
69
|
+
const { endUserId, endUserMetadata, sessionId, ...rest } = body;
|
|
70
|
+
const id = endUserId ?? this.endUserId;
|
|
71
71
|
const meta = endUserMetadata ?? this.endUserMetadata;
|
|
72
|
-
const
|
|
73
|
-
if (id === void 0 && meta === 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 (
|
|
84
|
-
attributes[SESSION_ID_KEY] = String(
|
|
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
|
}
|
package/dist/browser.mjs.map
CHANGED
|
@@ -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":[]}
|