@lmnr-ai/lmnr 0.8.44 → 0.8.45
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/cli.cjs +23 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +7 -8
- package/dist/cli.mjs.map +1 -1
- package/dist/{evaluations-RbBZb5yl.d.cts → evaluations-BHEE6fmA.d.cts} +17934 -8631
- package/dist/{evaluations-CVo0ms_r.d.mts → evaluations-BbIIQMLr.d.mts} +17934 -8631
- package/dist/{file-utils-1olUHMKC.mjs → file-utils-BtzEDUy6.mjs} +2 -2
- package/dist/file-utils-BtzEDUy6.mjs.map +1 -0
- package/dist/{file-utils-CMO5MuV9.cjs → file-utils-pZG20hHb.cjs} +6 -6
- package/dist/file-utils-pZG20hHb.cjs.map +1 -0
- package/dist/index.cjs +574 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1533 -437
- package/dist/index.d.mts +1533 -437
- package/dist/index.mjs +287 -148
- package/dist/index.mjs.map +1 -1
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.cjs +159 -62
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.cjs.map +1 -1
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.d.cts +3388 -250
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.d.mts +3388 -250
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.mjs +159 -62
- package/dist/opentelemetry-lib/instrumentation/temporal/workflow-interceptors.mjs.map +1 -1
- package/dist/{dist-BtB31bk1.cjs → utils-CatgN6Pb.cjs} +558 -558
- package/dist/utils-CatgN6Pb.cjs.map +1 -0
- package/dist/{dist-Cjres0a5.mjs → utils-UmlDPMxe.mjs} +552 -552
- package/dist/utils-UmlDPMxe.mjs.map +1 -0
- package/package.json +30 -30
- package/dist/dist-BtB31bk1.cjs.map +0 -1
- package/dist/dist-Cjres0a5.mjs.map +0 -1
- package/dist/file-utils-1olUHMKC.mjs.map +0 -1
- package/dist/file-utils-CMO5MuV9.cjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { TraceFlags } from "@opentelemetry/api";
|
|
2
1
|
import { config } from "dotenv";
|
|
3
2
|
import * as path from "path";
|
|
4
3
|
import pino from "pino";
|
|
5
4
|
import { PinoPretty } from "pino-pretty";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
5
|
import { v4 } from "uuid";
|
|
6
|
+
import { TraceFlags } from "@opentelemetry/api";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
8
|
//#region ../types/dist/index.mjs
|
|
9
9
|
/** Directory the debug-session file lives in, relative to the working dir. */
|
|
10
10
|
const DEBUG_SESSION_DIR = ".lmnr";
|
|
@@ -25,42 +25,128 @@ let TracingLevel = /* @__PURE__ */ function(TracingLevel) {
|
|
|
25
25
|
}({});
|
|
26
26
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
27
27
|
//#endregion
|
|
28
|
-
//#region
|
|
29
|
-
var version$1 = "0.8.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
28
|
+
//#region ../client/dist/index.mjs
|
|
29
|
+
var version$1 = "0.8.45";
|
|
30
|
+
function getLangVersion() {
|
|
31
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
32
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
var BaseResource = class {
|
|
36
|
+
constructor(baseHttpUrl, auth) {
|
|
37
|
+
this.baseHttpUrl = baseHttpUrl;
|
|
38
|
+
this.auth = auth;
|
|
39
|
+
this.credential = auth.type === "apiKey" ? auth.key : auth.token;
|
|
40
|
+
}
|
|
41
|
+
/** API path prefix: `/v1/cli` for CLI user-token auth, `/v1` otherwise. */
|
|
42
|
+
get apiPrefix() {
|
|
43
|
+
return this.auth.type === "userToken" ? "/v1/cli" : "/v1";
|
|
44
|
+
}
|
|
45
|
+
headers() {
|
|
46
|
+
return {
|
|
47
|
+
Authorization: `Bearer ${this.credential}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
Accept: "application/json",
|
|
50
|
+
...this.auth.type === "userToken" ? { "x-lmnr-project-id": this.auth.projectId } : {}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async handleError(response) {
|
|
54
|
+
const errorMsg = await response.text();
|
|
55
|
+
throw new Error(`${response.status} ${errorMsg}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var BrowserEventsResource = class extends BaseResource {
|
|
59
|
+
constructor(baseHttpUrl, auth) {
|
|
60
|
+
super(baseHttpUrl, auth);
|
|
61
|
+
}
|
|
62
|
+
async send({ sessionId, traceId, events }) {
|
|
63
|
+
const payload = {
|
|
64
|
+
sessionId,
|
|
65
|
+
traceId,
|
|
66
|
+
events,
|
|
67
|
+
source: getLangVersion() ?? "javascript",
|
|
68
|
+
sdkVersion: version$1
|
|
69
|
+
};
|
|
70
|
+
const jsonString = JSON.stringify(payload);
|
|
71
|
+
const compressedStream = new Blob([jsonString], { type: "application/json" }).stream().pipeThrough(new CompressionStream("gzip"));
|
|
72
|
+
const compressedData = await new Response(compressedStream).arrayBuffer();
|
|
73
|
+
const response = await fetch(this.baseHttpUrl + "/v1/browser-sessions/events", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
...this.headers(),
|
|
77
|
+
"Content-Encoding": "gzip"
|
|
78
|
+
},
|
|
79
|
+
body: compressedData
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) await this.handleError(response);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Locale-aware, case-insensitive comparator for the project listing. Ordering
|
|
86
|
+
* lives here — the single choke point every CLI surface (the `project list`
|
|
87
|
+
* table + its `--json`, and the interactive picker used by `setup` / `plugin
|
|
88
|
+
* add`) reads from — so the order can't drift between surfaces.
|
|
89
|
+
*/
|
|
90
|
+
const projectCollator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
91
|
+
/** Sort projects by workspace name, then project name (stable, human-scannable). */
|
|
92
|
+
const sortProjects = (projects) => [...projects].sort((a, b) => projectCollator.compare(a.workspaceName ?? "", b.workspaceName ?? "") || projectCollator.compare(a.name ?? "", b.name ?? ""));
|
|
93
|
+
/**
|
|
94
|
+
* User-scoped CLI endpoints that don't target a specific project. Authed by the
|
|
95
|
+
* BetterAuth user JWT (the `credential`); deliberately does NOT send an
|
|
96
|
+
* `x-lmnr-project-id` header (these routes are project discovery, pre-selection).
|
|
97
|
+
*
|
|
98
|
+
* Discovery exception: this resource always hits `/v1/cli/projects` with the
|
|
99
|
+
* bare bearer and overrides `BaseResource.headers()`/`apiPrefix`, so it works
|
|
100
|
+
* even when constructed with a `userToken` auth that has no real project id yet.
|
|
101
|
+
*/
|
|
102
|
+
var CliResource = class extends BaseResource {
|
|
103
|
+
constructor(baseHttpUrl, auth) {
|
|
104
|
+
super(baseHttpUrl, auth);
|
|
105
|
+
}
|
|
106
|
+
/** Workspaces + projects the authenticated user can access. */
|
|
107
|
+
async listProjects() {
|
|
108
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/cli/projects`, {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${this.credential}`,
|
|
112
|
+
Accept: "application/json"
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) await this.handleError(response);
|
|
116
|
+
const body = await response.json();
|
|
117
|
+
return Array.isArray(body?.projects) ? sortProjects(body.projects) : [];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve which project a project API key belongs to. Authed by the user JWT
|
|
121
|
+
* (the bearer); the project key travels in the body, NOT in `Authorization`.
|
|
122
|
+
* The server verifies the key and that the authenticated user is a member of
|
|
123
|
+
* the resolved project. Returns a tri-state probe so callers can distinguish a
|
|
124
|
+
* revoked key (401 → `invalid`) from a server/access problem (`unverifiable`).
|
|
125
|
+
*/
|
|
126
|
+
async resolveProjectByApiKey(apiKey) {
|
|
127
|
+
let response;
|
|
128
|
+
try {
|
|
129
|
+
response = await fetch(`${this.baseHttpUrl}/v1/cli/project`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${this.credential}`,
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
Accept: "application/json"
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ apiKey })
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return { status: "unverifiable" };
|
|
140
|
+
}
|
|
141
|
+
if (response.status === 401) return { status: "invalid" };
|
|
142
|
+
if (!response.ok) return { status: "unverifiable" };
|
|
143
|
+
const body = await response.json().catch(() => null);
|
|
144
|
+
return body?.projectId ? {
|
|
145
|
+
status: "ok",
|
|
146
|
+
projectId: body.projectId
|
|
147
|
+
} : { status: "unverifiable" };
|
|
148
|
+
}
|
|
61
149
|
};
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/utils.ts
|
|
64
150
|
function initializeLogger$1(options) {
|
|
65
151
|
const colorize = options?.colorize ?? true;
|
|
66
152
|
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
@@ -69,29 +155,18 @@ function initializeLogger$1(options) {
|
|
|
69
155
|
minimumLevel: level
|
|
70
156
|
}));
|
|
71
157
|
}
|
|
72
|
-
const logger$
|
|
158
|
+
const logger$4 = initializeLogger$1();
|
|
73
159
|
const isStringUUID$1 = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
|
|
74
|
-
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
75
160
|
const newUUID$1 = () => {
|
|
76
161
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
77
162
|
else return v4();
|
|
78
163
|
};
|
|
79
|
-
const normalizeOtelTraceId = (traceId) => {
|
|
80
|
-
let id = traceId.toLowerCase();
|
|
81
|
-
if (id.startsWith("0x")) id = id.slice(2);
|
|
82
|
-
return id.padStart(32, "0").slice(-32);
|
|
83
|
-
};
|
|
84
|
-
const normalizeOtelSpanId = (spanId) => {
|
|
85
|
-
let id = spanId.toLowerCase();
|
|
86
|
-
if (id.startsWith("0x")) id = id.slice(2);
|
|
87
|
-
return id.padStart(16, "0").slice(-16);
|
|
88
|
-
};
|
|
89
164
|
const otelSpanIdToUUID$1 = (spanId) => {
|
|
90
165
|
let id = spanId.toLowerCase();
|
|
91
166
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
92
|
-
if (id.length !== 16) logger$
|
|
167
|
+
if (id.length !== 16) logger$4.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
93
168
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
94
|
-
logger$
|
|
169
|
+
logger$4.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
95
170
|
return newUUID$1();
|
|
96
171
|
}
|
|
97
172
|
return id.padStart(32, "0").replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
@@ -99,481 +174,70 @@ const otelSpanIdToUUID$1 = (spanId) => {
|
|
|
99
174
|
const otelTraceIdToUUID$1 = (traceId) => {
|
|
100
175
|
let id = traceId.toLowerCase();
|
|
101
176
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
102
|
-
if (id.length !== 32) logger$
|
|
177
|
+
if (id.length !== 32) logger$4.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
103
178
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
104
|
-
logger$
|
|
179
|
+
logger$4.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
105
180
|
return newUUID$1();
|
|
106
181
|
}
|
|
107
182
|
return id.replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
108
183
|
};
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
184
|
+
const slicePayload = (value, length) => {
|
|
185
|
+
if (value === null || value === void 0) return value;
|
|
186
|
+
const str = JSON.stringify(value);
|
|
187
|
+
if (str.length <= length) return value;
|
|
188
|
+
return str.slice(0, length) + "...";
|
|
189
|
+
};
|
|
190
|
+
const loadEnv$1 = (options) => {
|
|
191
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
192
|
+
const envDir = process.cwd();
|
|
193
|
+
const envFiles = [
|
|
194
|
+
".env",
|
|
195
|
+
".env.local",
|
|
196
|
+
`.env.${nodeEnv}`,
|
|
197
|
+
`.env.${nodeEnv}.local`
|
|
198
|
+
];
|
|
199
|
+
const logLevel = process.env.LMNR_LOG_LEVEL ?? "info";
|
|
200
|
+
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
201
|
+
const quiet = options?.quiet ?? !verbose;
|
|
202
|
+
config({
|
|
203
|
+
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
204
|
+
quiet
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
const logger$3 = initializeLogger$1();
|
|
208
|
+
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
209
|
+
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
210
|
+
var DatasetsResource = class extends BaseResource {
|
|
211
|
+
constructor(baseHttpUrl, auth) {
|
|
212
|
+
super(baseHttpUrl, auth);
|
|
121
213
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
214
|
+
/**
|
|
215
|
+
* List all datasets.
|
|
216
|
+
*
|
|
217
|
+
* @returns {Promise<Dataset[]>} Array of datasets
|
|
218
|
+
*/
|
|
219
|
+
async listDatasets() {
|
|
220
|
+
const response = await fetch(this.baseHttpUrl + this.apiPrefix + "/datasets", {
|
|
221
|
+
method: "GET",
|
|
222
|
+
headers: this.headers()
|
|
129
223
|
});
|
|
224
|
+
if (!response.ok) await this.handleError(response);
|
|
225
|
+
return response.json();
|
|
130
226
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (typeof spanContext === "string") try {
|
|
146
|
-
return recordToOtelSpanContext(JSON.parse(spanContext));
|
|
147
|
-
} catch (e) {
|
|
148
|
-
throw new Error(`Failed to parse span context ${spanContext}. The string must be a json representation of a LaminarSpanContext.Error: ${errorMessage(e)}`);
|
|
149
|
-
}
|
|
150
|
-
else if (isRecord(spanContext)) return recordToOtelSpanContext(spanContext);
|
|
151
|
-
else if (typeof spanContext.traceId === "string" && typeof spanContext.spanId === "string" && spanContext.traceId.length === 32 && spanContext.spanId.length === 16) {
|
|
152
|
-
logger$5.warn("The span context is already an OpenTelemetry SpanContext. Returning it as is. Please use `LaminarSpanContext` objects instead.");
|
|
153
|
-
return spanContext;
|
|
154
|
-
} else throw new Error(`Invalid span context ${JSON.stringify(spanContext)}. Must be a LaminarSpanContext or its json representation.`);
|
|
155
|
-
};
|
|
156
|
-
const recordToOtelSpanContext = (record) => {
|
|
157
|
-
if (typeof record.spanId === "string" && typeof record.traceId === "string" || typeof record.span_id === "string" && typeof record.trace_id === "string") return {
|
|
158
|
-
spanId: uuidToOtelSpanId(record?.spanId ?? record?.["span_id"]),
|
|
159
|
-
traceId: uuidToOtelTraceId(record?.traceId ?? record?.["trace_id"]),
|
|
160
|
-
isRemote: record?.isRemote ?? record?.["is_remote"] ?? false,
|
|
161
|
-
traceFlags: record?.traceFlags ?? TraceFlags.SAMPLED
|
|
162
|
-
};
|
|
163
|
-
else throw new Error(`Invalid span context ${JSON.stringify(record)}. Must be a json representation of a LaminarSpanContext.`);
|
|
164
|
-
};
|
|
165
|
-
const isRecord = (value) => typeof value === "object" && !Array.isArray(value) && value !== null;
|
|
166
|
-
/**
|
|
167
|
-
* Deserialize a LaminarSpanContext from a string or record.
|
|
168
|
-
* Handles both camelCase and snake_case keys for cross-language compatibility.
|
|
169
|
-
*
|
|
170
|
-
* @param data - The data to deserialize (string or record)
|
|
171
|
-
* @returns The deserialized LaminarSpanContext
|
|
172
|
-
* @throws Error if the data is invalid
|
|
173
|
-
*/
|
|
174
|
-
const deserializeLaminarSpanContext = (data) => {
|
|
175
|
-
if (typeof data === "string") try {
|
|
176
|
-
return deserializeLaminarSpanContext(JSON.parse(data));
|
|
177
|
-
} catch (e) {
|
|
178
|
-
throw new Error(`Failed to parse LaminarSpanContext: ${errorMessage(e)}`);
|
|
179
|
-
}
|
|
180
|
-
if (!isRecord(data)) throw new Error("Invalid LaminarSpanContext: must be a string or object");
|
|
181
|
-
const traceId = data.traceId ?? data.trace_id;
|
|
182
|
-
const spanId = data.spanId ?? data.span_id;
|
|
183
|
-
const isRemote = data.isRemote ?? data.is_remote ?? false;
|
|
184
|
-
const spanPath = data.spanPath ?? data.span_path;
|
|
185
|
-
const spanIdsPath = data.spanIdsPath ?? data.span_ids_path;
|
|
186
|
-
const userId = data.userId ?? data.user_id;
|
|
187
|
-
const sessionId = data.sessionId ?? data.session_id;
|
|
188
|
-
const metadata = data.metadata;
|
|
189
|
-
const traceType = data.traceType ?? data.trace_type;
|
|
190
|
-
const tracingLevel = data.tracingLevel ?? data.tracing_level;
|
|
191
|
-
const debug = data.debug;
|
|
192
|
-
if (typeof traceId !== "string" || typeof spanId !== "string") throw new Error("Invalid LaminarSpanContext: traceId and spanId must be strings");
|
|
193
|
-
if (!isStringUUID$1(traceId) || !isStringUUID$1(spanId)) throw new Error("Invalid LaminarSpanContext: traceId and spanId must be valid UUIDs");
|
|
194
|
-
return {
|
|
195
|
-
traceId,
|
|
196
|
-
spanId,
|
|
197
|
-
isRemote: Boolean(isRemote),
|
|
198
|
-
spanPath: Array.isArray(spanPath) ? spanPath : void 0,
|
|
199
|
-
spanIdsPath: Array.isArray(spanIdsPath) ? spanIdsPath : void 0,
|
|
200
|
-
userId,
|
|
201
|
-
sessionId,
|
|
202
|
-
metadata,
|
|
203
|
-
traceType,
|
|
204
|
-
tracingLevel,
|
|
205
|
-
debug: isRecord(debug) ? deserializeDebugContext(debug) : void 0
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
/**
|
|
209
|
-
* Normalize a value to a canonical lowercase UUID string, or undefined.
|
|
210
|
-
*
|
|
211
|
-
* The debug block's `sessionId` / `replayTraceId` are always full ids; a value
|
|
212
|
-
* that isn't UUID-shaped is dropped (treated as absent) rather than thrown, so
|
|
213
|
-
* a partially-broken block never breaks span-context parsing.
|
|
214
|
-
*/
|
|
215
|
-
const asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
216
|
-
/**
|
|
217
|
-
* Parse a debug block, accepting camelCase and snake_case. All ids are kept
|
|
218
|
-
* VERBATIM: the producer emits the run's exact session / replay-trace /
|
|
219
|
-
* cache-until strings (un-normalized — `LMNR_DEBUG_SESSION_ID` may be an
|
|
220
|
-
* arbitrary non-UUID value), so the consumer must round-trip them unchanged or
|
|
221
|
-
* a downstream run never joins the run. Keep line-comparable with the Python
|
|
222
|
-
* `DebugContext.deserialize`.
|
|
223
|
-
*/
|
|
224
|
-
const deserializeDebugContext = (data) => ({
|
|
225
|
-
enabled: data.enabled === true,
|
|
226
|
-
sessionId: asString(data.sessionId ?? data.session_id),
|
|
227
|
-
replayTraceId: asString(data.replayTraceId ?? data.replay_trace_id),
|
|
228
|
-
cacheUntil: asString(data.cacheUntil ?? data.cache_until)
|
|
229
|
-
});
|
|
230
|
-
const getDirname = () => {
|
|
231
|
-
if (typeof __dirname !== "undefined") return __dirname;
|
|
232
|
-
if (typeof import.meta.url !== "undefined") return path.dirname(fileURLToPath(import.meta.url));
|
|
233
|
-
return process.cwd();
|
|
234
|
-
};
|
|
235
|
-
const MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 * 10;
|
|
236
|
-
const TRUNCATION_SUFFIX = "...[Laminar: truncated]";
|
|
237
|
-
/**
|
|
238
|
-
* Cut an oversized span payload down to the limit rather than dropping it.
|
|
239
|
-
*
|
|
240
|
-
* Keeping the leading bytes preserves the start of the value, which is the useful part when
|
|
241
|
-
* debugging. Mirrors `_truncate_payload` in the Python SDK.
|
|
242
|
-
*
|
|
243
|
-
* Callers pass the result of `JSON.stringify`, whose TS signature claims `string` but which
|
|
244
|
-
* actually returns `undefined` for a top-level `undefined`, function, or symbol. Guard at
|
|
245
|
-
* runtime and hand such values straight back: `setAttribute` treats them as a no-op, which is
|
|
246
|
-
* the behaviour these payloads had before truncation existed.
|
|
247
|
-
*/
|
|
248
|
-
const truncateSpanPayload = (serialized, kind) => {
|
|
249
|
-
if (typeof serialized !== "string") return serialized;
|
|
250
|
-
if (serialized.length <= 10485760) return serialized;
|
|
251
|
-
logger$5.warn(`Laminar: span ${kind} is ${serialized.length} bytes, which exceeds the ${MAX_MANUAL_SPAN_PAYLOAD_SIZE} byte limit. Truncating to the limit; the recorded value will not be valid JSON.`);
|
|
252
|
-
const keep = MAX_MANUAL_SPAN_PAYLOAD_SIZE - 23;
|
|
253
|
-
return serialized.slice(0, keep) + TRUNCATION_SUFFIX;
|
|
254
|
-
};
|
|
255
|
-
const isOtelAttributeValueType = (value) => {
|
|
256
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
|
|
257
|
-
if (Array.isArray(value)) {
|
|
258
|
-
const allStrings = value.every((value) => value == null || typeof value === "string");
|
|
259
|
-
const allNumbers = value.every((value) => value == null || typeof value === "number");
|
|
260
|
-
const allBooleans = value.every((value) => value == null || typeof value === "boolean");
|
|
261
|
-
return allStrings || allNumbers || allBooleans;
|
|
262
|
-
}
|
|
263
|
-
return false;
|
|
264
|
-
};
|
|
265
|
-
const metadataToAttributes = (metadata) => Object.fromEntries(Object.entries(metadata).map(([key, value]) => {
|
|
266
|
-
if (isOtelAttributeValueType(value)) return [`${ASSOCIATION_PROPERTIES}.metadata.${key}`, value];
|
|
267
|
-
else return [`${ASSOCIATION_PROPERTIES}.metadata.${key}`, JSON.stringify(value)];
|
|
268
|
-
}));
|
|
269
|
-
/**
|
|
270
|
-
* Get OTEL environment variable with priority order.
|
|
271
|
-
* Checks in order:
|
|
272
|
-
* 1. OTEL_EXPORTER_OTLP_TRACES_{varName}
|
|
273
|
-
* 2. OTEL_EXPORTER_OTLP_{varName}
|
|
274
|
-
* 3. OTEL_{varName}
|
|
275
|
-
*
|
|
276
|
-
* @param varName - The variable name (e.g., 'ENDPOINT', 'HEADERS', 'PROTOCOL')
|
|
277
|
-
* @returns The environment variable value or undefined if not found
|
|
278
|
-
*/
|
|
279
|
-
const getOtelEnvVar = (varName) => {
|
|
280
|
-
const candidates = [
|
|
281
|
-
`OTEL_EXPORTER_OTLP_TRACES_${varName}`,
|
|
282
|
-
`OTEL_EXPORTER_OTLP_${varName}`,
|
|
283
|
-
`OTEL_${varName}`
|
|
284
|
-
];
|
|
285
|
-
for (const candidate of candidates) {
|
|
286
|
-
const value = process?.env?.[candidate];
|
|
287
|
-
if (value) return value;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
/**
|
|
291
|
-
* Check if OTEL configuration is available.
|
|
292
|
-
* @returns true if OTEL endpoint is configured
|
|
293
|
-
*/
|
|
294
|
-
const hasOtelConfig = () => !!getOtelEnvVar("ENDPOINT");
|
|
295
|
-
/**
|
|
296
|
-
* Parse OTEL headers string into a record object.
|
|
297
|
-
* Format: key1=value1,key2=value2
|
|
298
|
-
* Values are URL-decoded.
|
|
299
|
-
*
|
|
300
|
-
* @param headersStr - Headers string in OTEL format
|
|
301
|
-
* @returns Parsed headers object
|
|
302
|
-
*/
|
|
303
|
-
const parseOtelHeaders = (headersStr) => {
|
|
304
|
-
if (!headersStr) return {};
|
|
305
|
-
const headers = {};
|
|
306
|
-
for (const pair of headersStr.split(",")) {
|
|
307
|
-
const equalIndex = pair.indexOf("=");
|
|
308
|
-
if (equalIndex !== -1) {
|
|
309
|
-
const key = pair.substring(0, equalIndex).trim();
|
|
310
|
-
const value = pair.substring(equalIndex + 1).trim();
|
|
311
|
-
headers[key] = decodeURIComponent(value);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return headers;
|
|
315
|
-
};
|
|
316
|
-
/**
|
|
317
|
-
* Validate that either Laminar API key or OTEL configuration is present.
|
|
318
|
-
* Throws an error if neither is configured.
|
|
319
|
-
*
|
|
320
|
-
* @param apiKey - The Laminar API key (if provided)
|
|
321
|
-
* @throws Error if neither API key nor OTEL configuration is present
|
|
322
|
-
*/
|
|
323
|
-
const validateTracingConfig = (apiKey) => {
|
|
324
|
-
if (!apiKey && !hasOtelConfig()) throw new Error("Please initialize the Laminar object with your project API key or set the LMNR_PROJECT_API_KEY environment variable, or configure OTEL environment variables (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, etc.)");
|
|
325
|
-
};
|
|
326
|
-
const loadEnv$1 = (options) => {
|
|
327
|
-
const nodeEnv = process.env.NODE_ENV || "development";
|
|
328
|
-
const envDir = process.cwd();
|
|
329
|
-
const envFiles = [
|
|
330
|
-
".env",
|
|
331
|
-
".env.local",
|
|
332
|
-
`.env.${nodeEnv}`,
|
|
333
|
-
`.env.${nodeEnv}.local`
|
|
334
|
-
];
|
|
335
|
-
const logLevel = process.env.LMNR_LOG_LEVEL ?? "info";
|
|
336
|
-
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
337
|
-
const quiet = options?.quiet ?? !verbose;
|
|
338
|
-
config({
|
|
339
|
-
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
340
|
-
quiet
|
|
341
|
-
});
|
|
342
|
-
};
|
|
343
|
-
/**
|
|
344
|
-
* Converts an API base URL to the frontend/web URL.
|
|
345
|
-
* - Converts https://api.lmnr.ai to https://www.laminar.sh
|
|
346
|
-
* - Removes trailing slashes
|
|
347
|
-
* - For localhost/127.0.0.1, ensures a port is specified (defaults to 5667)
|
|
348
|
-
*
|
|
349
|
-
* @param baseUrl - The API base URL (defaults to "https://api.lmnr.ai")
|
|
350
|
-
* @returns The frontend URL
|
|
351
|
-
*/
|
|
352
|
-
const getFrontendUrl = (baseUrl, frontendPort) => {
|
|
353
|
-
let url = baseUrl ?? "https://api.lmnr.ai";
|
|
354
|
-
if (url === "https://api.lmnr.ai") url = "https://www.laminar.sh";
|
|
355
|
-
url = url.replace(/\/$/, "");
|
|
356
|
-
if (/localhost|127\.0\.0\.1/.test(url)) {
|
|
357
|
-
const port = frontendPort ?? url.match(/:\d{1,5}$/g)?.[0]?.slice(1) ?? 5667;
|
|
358
|
-
if (/:(\d{1,5})$/.test(url)) url = url.replace(/:\d{1,5}$/g, `:${port}`);
|
|
359
|
-
else url = `${url}:${port}`;
|
|
360
|
-
}
|
|
361
|
-
return url;
|
|
362
|
-
};
|
|
363
|
-
//#endregion
|
|
364
|
-
//#region ../client/dist/index.mjs
|
|
365
|
-
var version = "0.8.44";
|
|
366
|
-
function getLangVersion() {
|
|
367
|
-
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
368
|
-
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
var BaseResource = class {
|
|
372
|
-
constructor(baseHttpUrl, auth) {
|
|
373
|
-
this.baseHttpUrl = baseHttpUrl;
|
|
374
|
-
this.auth = auth;
|
|
375
|
-
this.credential = auth.type === "apiKey" ? auth.key : auth.token;
|
|
376
|
-
}
|
|
377
|
-
/** API path prefix: `/v1/cli` for CLI user-token auth, `/v1` otherwise. */
|
|
378
|
-
get apiPrefix() {
|
|
379
|
-
return this.auth.type === "userToken" ? "/v1/cli" : "/v1";
|
|
380
|
-
}
|
|
381
|
-
headers() {
|
|
382
|
-
return {
|
|
383
|
-
Authorization: `Bearer ${this.credential}`,
|
|
384
|
-
"Content-Type": "application/json",
|
|
385
|
-
Accept: "application/json",
|
|
386
|
-
...this.auth.type === "userToken" ? { "x-lmnr-project-id": this.auth.projectId } : {}
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
async handleError(response) {
|
|
390
|
-
const errorMsg = await response.text();
|
|
391
|
-
throw new Error(`${response.status} ${errorMsg}`);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
var BrowserEventsResource = class extends BaseResource {
|
|
395
|
-
constructor(baseHttpUrl, auth) {
|
|
396
|
-
super(baseHttpUrl, auth);
|
|
397
|
-
}
|
|
398
|
-
async send({ sessionId, traceId, events }) {
|
|
399
|
-
const payload = {
|
|
400
|
-
sessionId,
|
|
401
|
-
traceId,
|
|
402
|
-
events,
|
|
403
|
-
source: getLangVersion() ?? "javascript",
|
|
404
|
-
sdkVersion: version
|
|
405
|
-
};
|
|
406
|
-
const jsonString = JSON.stringify(payload);
|
|
407
|
-
const compressedStream = new Blob([jsonString], { type: "application/json" }).stream().pipeThrough(new CompressionStream("gzip"));
|
|
408
|
-
const compressedData = await new Response(compressedStream).arrayBuffer();
|
|
409
|
-
const response = await fetch(this.baseHttpUrl + "/v1/browser-sessions/events", {
|
|
410
|
-
method: "POST",
|
|
411
|
-
headers: {
|
|
412
|
-
...this.headers(),
|
|
413
|
-
"Content-Encoding": "gzip"
|
|
414
|
-
},
|
|
415
|
-
body: compressedData
|
|
416
|
-
});
|
|
417
|
-
if (!response.ok) await this.handleError(response);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
/**
|
|
421
|
-
* Locale-aware, case-insensitive comparator for the project listing. Ordering
|
|
422
|
-
* lives here — the single choke point every CLI surface (the `project list`
|
|
423
|
-
* table + its `--json`, and the interactive picker used by `setup` / `plugin
|
|
424
|
-
* add`) reads from — so the order can't drift between surfaces.
|
|
425
|
-
*/
|
|
426
|
-
const projectCollator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
427
|
-
/** Sort projects by workspace name, then project name (stable, human-scannable). */
|
|
428
|
-
const sortProjects = (projects) => [...projects].sort((a, b) => projectCollator.compare(a.workspaceName ?? "", b.workspaceName ?? "") || projectCollator.compare(a.name ?? "", b.name ?? ""));
|
|
429
|
-
/**
|
|
430
|
-
* User-scoped CLI endpoints that don't target a specific project. Authed by the
|
|
431
|
-
* BetterAuth user JWT (the `credential`); deliberately does NOT send an
|
|
432
|
-
* `x-lmnr-project-id` header (these routes are project discovery, pre-selection).
|
|
433
|
-
*
|
|
434
|
-
* Discovery exception: this resource always hits `/v1/cli/projects` with the
|
|
435
|
-
* bare bearer and overrides `BaseResource.headers()`/`apiPrefix`, so it works
|
|
436
|
-
* even when constructed with a `userToken` auth that has no real project id yet.
|
|
437
|
-
*/
|
|
438
|
-
var CliResource = class extends BaseResource {
|
|
439
|
-
constructor(baseHttpUrl, auth) {
|
|
440
|
-
super(baseHttpUrl, auth);
|
|
441
|
-
}
|
|
442
|
-
/** Workspaces + projects the authenticated user can access. */
|
|
443
|
-
async listProjects() {
|
|
444
|
-
const response = await fetch(`${this.baseHttpUrl}/v1/cli/projects`, {
|
|
445
|
-
method: "GET",
|
|
446
|
-
headers: {
|
|
447
|
-
Authorization: `Bearer ${this.credential}`,
|
|
448
|
-
Accept: "application/json"
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
if (!response.ok) await this.handleError(response);
|
|
452
|
-
const body = await response.json();
|
|
453
|
-
return Array.isArray(body?.projects) ? sortProjects(body.projects) : [];
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Resolve which project a project API key belongs to. Authed by the user JWT
|
|
457
|
-
* (the bearer); the project key travels in the body, NOT in `Authorization`.
|
|
458
|
-
* The server verifies the key and that the authenticated user is a member of
|
|
459
|
-
* the resolved project. Returns a tri-state probe so callers can distinguish a
|
|
460
|
-
* revoked key (401 → `invalid`) from a server/access problem (`unverifiable`).
|
|
461
|
-
*/
|
|
462
|
-
async resolveProjectByApiKey(apiKey) {
|
|
463
|
-
let response;
|
|
464
|
-
try {
|
|
465
|
-
response = await fetch(`${this.baseHttpUrl}/v1/cli/project`, {
|
|
466
|
-
method: "POST",
|
|
467
|
-
headers: {
|
|
468
|
-
Authorization: `Bearer ${this.credential}`,
|
|
469
|
-
"Content-Type": "application/json",
|
|
470
|
-
Accept: "application/json"
|
|
471
|
-
},
|
|
472
|
-
body: JSON.stringify({ apiKey })
|
|
473
|
-
});
|
|
474
|
-
} catch {
|
|
475
|
-
return { status: "unverifiable" };
|
|
476
|
-
}
|
|
477
|
-
if (response.status === 401) return { status: "invalid" };
|
|
478
|
-
if (!response.ok) return { status: "unverifiable" };
|
|
479
|
-
const body = await response.json().catch(() => null);
|
|
480
|
-
return body?.projectId ? {
|
|
481
|
-
status: "ok",
|
|
482
|
-
projectId: body.projectId
|
|
483
|
-
} : { status: "unverifiable" };
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
function initializeLogger(options) {
|
|
487
|
-
const colorize = options?.colorize ?? true;
|
|
488
|
-
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
489
|
-
return pino({ level }, PinoPretty({
|
|
490
|
-
colorize,
|
|
491
|
-
minimumLevel: level
|
|
492
|
-
}));
|
|
493
|
-
}
|
|
494
|
-
const logger$4 = initializeLogger();
|
|
495
|
-
const isStringUUID = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
|
|
496
|
-
const newUUID = () => {
|
|
497
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
498
|
-
else return v4();
|
|
499
|
-
};
|
|
500
|
-
const otelSpanIdToUUID = (spanId) => {
|
|
501
|
-
let id = spanId.toLowerCase();
|
|
502
|
-
if (id.startsWith("0x")) id = id.slice(2);
|
|
503
|
-
if (id.length !== 16) logger$4.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
504
|
-
if (!/^[0-9a-f]+$/.test(id)) {
|
|
505
|
-
logger$4.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
506
|
-
return newUUID();
|
|
507
|
-
}
|
|
508
|
-
return id.padStart(32, "0").replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
509
|
-
};
|
|
510
|
-
const otelTraceIdToUUID = (traceId) => {
|
|
511
|
-
let id = traceId.toLowerCase();
|
|
512
|
-
if (id.startsWith("0x")) id = id.slice(2);
|
|
513
|
-
if (id.length !== 32) logger$4.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
514
|
-
if (!/^[0-9a-f]+$/.test(id)) {
|
|
515
|
-
logger$4.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
516
|
-
return newUUID();
|
|
517
|
-
}
|
|
518
|
-
return id.replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
519
|
-
};
|
|
520
|
-
const slicePayload = (value, length) => {
|
|
521
|
-
if (value === null || value === void 0) return value;
|
|
522
|
-
const str = JSON.stringify(value);
|
|
523
|
-
if (str.length <= length) return value;
|
|
524
|
-
return str.slice(0, length) + "...";
|
|
525
|
-
};
|
|
526
|
-
const loadEnv = (options) => {
|
|
527
|
-
const nodeEnv = process.env.NODE_ENV || "development";
|
|
528
|
-
const envDir = process.cwd();
|
|
529
|
-
const envFiles = [
|
|
530
|
-
".env",
|
|
531
|
-
".env.local",
|
|
532
|
-
`.env.${nodeEnv}`,
|
|
533
|
-
`.env.${nodeEnv}.local`
|
|
534
|
-
];
|
|
535
|
-
const logLevel = process.env.LMNR_LOG_LEVEL ?? "info";
|
|
536
|
-
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
537
|
-
const quiet = options?.quiet ?? !verbose;
|
|
538
|
-
config({
|
|
539
|
-
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
540
|
-
quiet
|
|
541
|
-
});
|
|
542
|
-
};
|
|
543
|
-
const logger$3 = initializeLogger();
|
|
544
|
-
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
545
|
-
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
546
|
-
var DatasetsResource = class extends BaseResource {
|
|
547
|
-
constructor(baseHttpUrl, auth) {
|
|
548
|
-
super(baseHttpUrl, auth);
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* List all datasets.
|
|
552
|
-
*
|
|
553
|
-
* @returns {Promise<Dataset[]>} Array of datasets
|
|
554
|
-
*/
|
|
555
|
-
async listDatasets() {
|
|
556
|
-
const response = await fetch(this.baseHttpUrl + this.apiPrefix + "/datasets", {
|
|
557
|
-
method: "GET",
|
|
558
|
-
headers: this.headers()
|
|
559
|
-
});
|
|
560
|
-
if (!response.ok) await this.handleError(response);
|
|
561
|
-
return response.json();
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Get a dataset by name.
|
|
565
|
-
*
|
|
566
|
-
* @param {string} name - Name of the dataset
|
|
567
|
-
* @returns {Promise<Dataset[]>} Array of datasets with matching name
|
|
568
|
-
*/
|
|
569
|
-
async getDatasetByName(name) {
|
|
570
|
-
const params = new URLSearchParams({ name });
|
|
571
|
-
const response = await fetch(this.baseHttpUrl + `${this.apiPrefix}/datasets?${params.toString()}`, {
|
|
572
|
-
method: "GET",
|
|
573
|
-
headers: this.headers()
|
|
574
|
-
});
|
|
575
|
-
if (!response.ok) await this.handleError(response);
|
|
576
|
-
return response.json();
|
|
227
|
+
/**
|
|
228
|
+
* Get a dataset by name.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} name - Name of the dataset
|
|
231
|
+
* @returns {Promise<Dataset[]>} Array of datasets with matching name
|
|
232
|
+
*/
|
|
233
|
+
async getDatasetByName(name) {
|
|
234
|
+
const params = new URLSearchParams({ name });
|
|
235
|
+
const response = await fetch(this.baseHttpUrl + `${this.apiPrefix}/datasets?${params.toString()}`, {
|
|
236
|
+
method: "GET",
|
|
237
|
+
headers: this.headers()
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) await this.handleError(response);
|
|
240
|
+
return response.json();
|
|
577
241
|
}
|
|
578
242
|
/**
|
|
579
243
|
* Push datapoints to a dataset.
|
|
@@ -643,7 +307,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
643
307
|
return response.json();
|
|
644
308
|
}
|
|
645
309
|
};
|
|
646
|
-
const logger$2 = initializeLogger();
|
|
310
|
+
const logger$2 = initializeLogger$1();
|
|
647
311
|
const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
648
312
|
var EvalsResource = class extends BaseResource {
|
|
649
313
|
constructor(baseHttpUrl, auth) {
|
|
@@ -723,14 +387,14 @@ var EvalsResource = class extends BaseResource {
|
|
|
723
387
|
* @returns {Promise<StringUUID>} The datapoint ID
|
|
724
388
|
*/
|
|
725
389
|
async createDatapoint({ evalId, data, target, metadata, index, traceId }) {
|
|
726
|
-
const datapointId = newUUID();
|
|
390
|
+
const datapointId = newUUID$1();
|
|
727
391
|
const partialDatapoint = {
|
|
728
392
|
id: datapointId,
|
|
729
393
|
data,
|
|
730
394
|
target,
|
|
731
395
|
index: index ?? 0,
|
|
732
|
-
traceId: traceId ?? newUUID(),
|
|
733
|
-
executorSpanId: newUUID(),
|
|
396
|
+
traceId: traceId ?? newUUID$1(),
|
|
397
|
+
executorSpanId: newUUID$1(),
|
|
734
398
|
metadata
|
|
735
399
|
};
|
|
736
400
|
await this.saveDatapoints({
|
|
@@ -882,14 +546,14 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
882
546
|
metadata,
|
|
883
547
|
score,
|
|
884
548
|
source: "Code",
|
|
885
|
-
traceId: isStringUUID(options.traceId) ? options.traceId : otelTraceIdToUUID(options.traceId)
|
|
549
|
+
traceId: isStringUUID$1(options.traceId) ? options.traceId : otelTraceIdToUUID$1(options.traceId)
|
|
886
550
|
};
|
|
887
551
|
else if ("spanId" in options && options.spanId) payload = {
|
|
888
552
|
name,
|
|
889
553
|
metadata,
|
|
890
554
|
score,
|
|
891
555
|
source: "Code",
|
|
892
|
-
spanId: isStringUUID(options.spanId) ? options.spanId : otelSpanIdToUUID(options.spanId)
|
|
556
|
+
spanId: isStringUUID$1(options.spanId) ? options.spanId : otelSpanIdToUUID$1(options.spanId)
|
|
893
557
|
};
|
|
894
558
|
else throw new Error("Either 'traceId' or 'spanId' must be provided.");
|
|
895
559
|
const response = await fetch(this.baseHttpUrl + "/v1/evaluators/score", {
|
|
@@ -900,7 +564,7 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
900
564
|
if (!response.ok) await this.handleError(response);
|
|
901
565
|
}
|
|
902
566
|
};
|
|
903
|
-
const logger$1 = initializeLogger();
|
|
567
|
+
const logger$1 = initializeLogger$1();
|
|
904
568
|
/**
|
|
905
569
|
* Map the opaque HIT `response` payload onto a {@link CachedSpan} the provider
|
|
906
570
|
* wrappers can replay. The server-side shape of `response` is not yet frozen
|
|
@@ -1230,11 +894,11 @@ var TagsResource = class extends BaseResource {
|
|
|
1230
894
|
*/
|
|
1231
895
|
async tag(trace_id, tags) {
|
|
1232
896
|
const traceTags = Array.isArray(tags) ? tags : [tags];
|
|
1233
|
-
const formattedTraceId = isStringUUID(trace_id) ? trace_id : otelTraceIdToUUID(trace_id);
|
|
897
|
+
const formattedTraceId = isStringUUID$1(trace_id) ? trace_id : otelTraceIdToUUID$1(trace_id);
|
|
1234
898
|
const url = this.baseHttpUrl + "/v1/tag";
|
|
1235
899
|
const payload = {
|
|
1236
|
-
|
|
1237
|
-
|
|
900
|
+
traceId: formattedTraceId,
|
|
901
|
+
names: traceTags
|
|
1238
902
|
};
|
|
1239
903
|
const response = await fetch(url, {
|
|
1240
904
|
method: "POST",
|
|
@@ -1246,7 +910,7 @@ var TagsResource = class extends BaseResource {
|
|
|
1246
910
|
}
|
|
1247
911
|
};
|
|
1248
912
|
/** Resource for post-factum operations on existing traces. */
|
|
1249
|
-
const logger = initializeLogger();
|
|
913
|
+
const logger$5 = initializeLogger$1();
|
|
1250
914
|
var TracesResource = class extends BaseResource {
|
|
1251
915
|
/** Resource for post-factum operations on existing traces. */
|
|
1252
916
|
constructor(baseHttpUrl, auth) {
|
|
@@ -1304,7 +968,7 @@ var TracesResource = class extends BaseResource {
|
|
|
1304
968
|
*/
|
|
1305
969
|
async pushMetadata(traceId, metadata, options) {
|
|
1306
970
|
if (!metadata || Object.keys(metadata).length === 0) throw new Error("metadata must be a non-empty object");
|
|
1307
|
-
const formattedTraceId = isStringUUID(traceId) ? traceId : otelTraceIdToUUID(traceId);
|
|
971
|
+
const formattedTraceId = isStringUUID$1(traceId) ? traceId : otelTraceIdToUUID$1(traceId);
|
|
1308
972
|
const url = this.baseHttpUrl + this.apiPrefix + "/traces/metadata";
|
|
1309
973
|
const response = await fetch(url, {
|
|
1310
974
|
method: "POST",
|
|
@@ -1317,7 +981,7 @@ var TracesResource = class extends BaseResource {
|
|
|
1317
981
|
if (response.status === 404) {
|
|
1318
982
|
const message = `Trace ${formattedTraceId} not found. The trace may not have been flushed yet — call await Laminar.flush() and retry.`;
|
|
1319
983
|
if (options?.failOnNotFound) throw new Error(message);
|
|
1320
|
-
logger.warn(message);
|
|
984
|
+
logger$5.warn(message);
|
|
1321
985
|
return;
|
|
1322
986
|
}
|
|
1323
987
|
if (!response.ok) await this.handleError(response);
|
|
@@ -1325,7 +989,7 @@ var TracesResource = class extends BaseResource {
|
|
|
1325
989
|
};
|
|
1326
990
|
var LaminarClient = class LaminarClient {
|
|
1327
991
|
constructor({ baseUrl, port, auth, projectApiKey, cliUserProjectId } = {}) {
|
|
1328
|
-
loadEnv();
|
|
992
|
+
loadEnv$1();
|
|
1329
993
|
this.auth = LaminarClient.normalizeAuth(auth, projectApiKey, cliUserProjectId);
|
|
1330
994
|
const resolvedBaseUrl = baseUrl ?? process.env.LMNR_BASE_URL;
|
|
1331
995
|
this.configuredUrl = resolvedBaseUrl;
|
|
@@ -1395,35 +1059,371 @@ var LaminarClient = class LaminarClient {
|
|
|
1395
1059
|
get browserEvents() {
|
|
1396
1060
|
return this._browserEvents;
|
|
1397
1061
|
}
|
|
1398
|
-
get cli() {
|
|
1399
|
-
return this._cli;
|
|
1062
|
+
get cli() {
|
|
1063
|
+
return this._cli;
|
|
1064
|
+
}
|
|
1065
|
+
get datasets() {
|
|
1066
|
+
return this._datasets;
|
|
1067
|
+
}
|
|
1068
|
+
get evals() {
|
|
1069
|
+
return this._evals;
|
|
1070
|
+
}
|
|
1071
|
+
get evaluators() {
|
|
1072
|
+
return this._evaluators;
|
|
1073
|
+
}
|
|
1074
|
+
get signals() {
|
|
1075
|
+
return this._signals;
|
|
1076
|
+
}
|
|
1077
|
+
get rolloutSessions() {
|
|
1078
|
+
return this._rolloutSessions;
|
|
1079
|
+
}
|
|
1080
|
+
get sql() {
|
|
1081
|
+
return this._sql;
|
|
1082
|
+
}
|
|
1083
|
+
get tags() {
|
|
1084
|
+
return this._tags;
|
|
1085
|
+
}
|
|
1086
|
+
get traces() {
|
|
1087
|
+
return this._traces;
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region package.json
|
|
1092
|
+
var version = "0.8.45";
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region src/opentelemetry-lib/tracing/attributes.ts
|
|
1095
|
+
const SPAN_INPUT = "lmnr.span.input";
|
|
1096
|
+
const SPAN_OUTPUT = "lmnr.span.output";
|
|
1097
|
+
const SPAN_TYPE = "lmnr.span.type";
|
|
1098
|
+
const SPAN_PATH = "lmnr.span.path";
|
|
1099
|
+
const SPAN_IDS_PATH = "lmnr.span.ids_path";
|
|
1100
|
+
const PARENT_SPAN_PATH = "lmnr.span.parent_path";
|
|
1101
|
+
const PARENT_SPAN_IDS_PATH = "lmnr.span.parent_ids_path";
|
|
1102
|
+
const SPAN_INSTRUMENTATION_SOURCE = "lmnr.span.instrumentation_source";
|
|
1103
|
+
const SPAN_SDK_VERSION = "lmnr.span.sdk_version";
|
|
1104
|
+
const SPAN_LANGUAGE_VERSION = "lmnr.span.language_version";
|
|
1105
|
+
const SPAN_INSTRUMENTATION_SCOPE_NAME = "lmnr.span.instrumentation_scope.name";
|
|
1106
|
+
const SPAN_INSTRUMENTATION_SCOPE_VERSION = "lmnr.span.instrumentation_scope.version";
|
|
1107
|
+
const TRACE_HAS_BROWSER_SESSION = "lmnr.internal.has_browser_session";
|
|
1108
|
+
const HUMAN_EVALUATOR_OPTIONS = "lmnr.span.human_evaluator_options";
|
|
1109
|
+
const ASSOCIATION_PROPERTIES = "lmnr.association.properties";
|
|
1110
|
+
const SESSION_ID = "lmnr.association.properties.session_id";
|
|
1111
|
+
const USER_ID = "lmnr.association.properties.user_id";
|
|
1112
|
+
const TRACE_TYPE = "lmnr.association.properties.trace_type";
|
|
1113
|
+
const ASSOCIATION_PROPERTIES_OVERRIDES = { span_type: SPAN_TYPE };
|
|
1114
|
+
const LaminarAttributes = {
|
|
1115
|
+
INPUT_TOKEN_COUNT: "gen_ai.usage.input_tokens",
|
|
1116
|
+
OUTPUT_TOKEN_COUNT: "gen_ai.usage.output_tokens",
|
|
1117
|
+
TOTAL_TOKEN_COUNT: "llm.usage.total_tokens",
|
|
1118
|
+
PROVIDER: "gen_ai.system",
|
|
1119
|
+
REQUEST_MODEL: "gen_ai.request.model",
|
|
1120
|
+
RESPONSE_MODEL: "gen_ai.response.model",
|
|
1121
|
+
INPUT_COST: "gen_ai.usage.input_cost",
|
|
1122
|
+
OUTPUT_COST: "gen_ai.usage.output_cost",
|
|
1123
|
+
TOTAL_COST: "gen_ai.usage.cost"
|
|
1124
|
+
};
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/utils.ts
|
|
1127
|
+
function initializeLogger(options) {
|
|
1128
|
+
const colorize = options?.colorize ?? true;
|
|
1129
|
+
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
1130
|
+
return pino({ level }, PinoPretty({
|
|
1131
|
+
colorize,
|
|
1132
|
+
minimumLevel: level
|
|
1133
|
+
}));
|
|
1134
|
+
}
|
|
1135
|
+
const logger = initializeLogger();
|
|
1136
|
+
const isStringUUID = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
|
|
1137
|
+
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
1138
|
+
const newUUID = () => {
|
|
1139
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
1140
|
+
else return v4();
|
|
1141
|
+
};
|
|
1142
|
+
const normalizeOtelTraceId = (traceId) => {
|
|
1143
|
+
let id = traceId.toLowerCase();
|
|
1144
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
1145
|
+
return id.padStart(32, "0").slice(-32);
|
|
1146
|
+
};
|
|
1147
|
+
const normalizeOtelSpanId = (spanId) => {
|
|
1148
|
+
let id = spanId.toLowerCase();
|
|
1149
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
1150
|
+
return id.padStart(16, "0").slice(-16);
|
|
1151
|
+
};
|
|
1152
|
+
const otelSpanIdToUUID = (spanId) => {
|
|
1153
|
+
let id = spanId.toLowerCase();
|
|
1154
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
1155
|
+
if (id.length !== 16) logger.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
1156
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
1157
|
+
logger.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
1158
|
+
return newUUID();
|
|
1159
|
+
}
|
|
1160
|
+
return id.padStart(32, "0").replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
1161
|
+
};
|
|
1162
|
+
const otelTraceIdToUUID = (traceId) => {
|
|
1163
|
+
let id = traceId.toLowerCase();
|
|
1164
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
1165
|
+
if (id.length !== 32) logger.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
1166
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
1167
|
+
logger.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
1168
|
+
return newUUID();
|
|
1169
|
+
}
|
|
1170
|
+
return id.replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
1171
|
+
};
|
|
1172
|
+
const uuidToOtelTraceId = (uuid) => uuid.replace(/-/g, "");
|
|
1173
|
+
const uuidToOtelSpanId = (uuid) => uuid.replace(/-/g, "").slice(16);
|
|
1174
|
+
/**
|
|
1175
|
+
* This is a simple implementation of a semaphore to replicate
|
|
1176
|
+
* the behavior of the `asyncio.Semaphore` in Python.
|
|
1177
|
+
*/
|
|
1178
|
+
var Semaphore = class {
|
|
1179
|
+
constructor(value = 1) {
|
|
1180
|
+
this._waiters = [];
|
|
1181
|
+
if (value < 0) throw new Error("Semaphore value must be >= 0");
|
|
1182
|
+
this._value = value;
|
|
1183
|
+
this._waiters = [];
|
|
1184
|
+
}
|
|
1185
|
+
async acquire() {
|
|
1186
|
+
if (this._value > 0) {
|
|
1187
|
+
this._value--;
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
return new Promise((resolve) => {
|
|
1191
|
+
this._waiters.push(resolve);
|
|
1192
|
+
});
|
|
1400
1193
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1194
|
+
release() {
|
|
1195
|
+
if (this._waiters.length > 0) this._waiters.shift()?.();
|
|
1196
|
+
else this._value++;
|
|
1403
1197
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1198
|
+
async using(fn) {
|
|
1199
|
+
try {
|
|
1200
|
+
await this.acquire();
|
|
1201
|
+
return await fn();
|
|
1202
|
+
} finally {
|
|
1203
|
+
this.release();
|
|
1204
|
+
}
|
|
1406
1205
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1206
|
+
};
|
|
1207
|
+
const tryToOtelSpanContext = (spanContext) => {
|
|
1208
|
+
if (typeof spanContext === "string") try {
|
|
1209
|
+
return recordToOtelSpanContext(JSON.parse(spanContext));
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
throw new Error(`Failed to parse span context ${spanContext}. The string must be a json representation of a LaminarSpanContext.Error: ${errorMessage(e)}`);
|
|
1409
1212
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1213
|
+
else if (isRecord(spanContext)) return recordToOtelSpanContext(spanContext);
|
|
1214
|
+
else if (typeof spanContext.traceId === "string" && typeof spanContext.spanId === "string" && spanContext.traceId.length === 32 && spanContext.spanId.length === 16) {
|
|
1215
|
+
logger.warn("The span context is already an OpenTelemetry SpanContext. Returning it as is. Please use `LaminarSpanContext` objects instead.");
|
|
1216
|
+
return spanContext;
|
|
1217
|
+
} else throw new Error(`Invalid span context ${JSON.stringify(spanContext)}. Must be a LaminarSpanContext or its json representation.`);
|
|
1218
|
+
};
|
|
1219
|
+
const recordToOtelSpanContext = (record) => {
|
|
1220
|
+
if (typeof record.spanId === "string" && typeof record.traceId === "string" || typeof record.span_id === "string" && typeof record.trace_id === "string") return {
|
|
1221
|
+
spanId: uuidToOtelSpanId(record?.spanId ?? record?.["span_id"]),
|
|
1222
|
+
traceId: uuidToOtelTraceId(record?.traceId ?? record?.["trace_id"]),
|
|
1223
|
+
isRemote: record?.isRemote ?? record?.["is_remote"] ?? false,
|
|
1224
|
+
traceFlags: record?.traceFlags ?? TraceFlags.SAMPLED
|
|
1225
|
+
};
|
|
1226
|
+
else throw new Error(`Invalid span context ${JSON.stringify(record)}. Must be a json representation of a LaminarSpanContext.`);
|
|
1227
|
+
};
|
|
1228
|
+
const isRecord = (value) => typeof value === "object" && !Array.isArray(value) && value !== null;
|
|
1229
|
+
/**
|
|
1230
|
+
* Deserialize a LaminarSpanContext from a string or record.
|
|
1231
|
+
* Handles both camelCase and snake_case keys for cross-language compatibility.
|
|
1232
|
+
*
|
|
1233
|
+
* @param data - The data to deserialize (string or record)
|
|
1234
|
+
* @returns The deserialized LaminarSpanContext
|
|
1235
|
+
* @throws Error if the data is invalid
|
|
1236
|
+
*/
|
|
1237
|
+
const deserializeLaminarSpanContext = (data) => {
|
|
1238
|
+
if (typeof data === "string") try {
|
|
1239
|
+
return deserializeLaminarSpanContext(JSON.parse(data));
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
throw new Error(`Failed to parse LaminarSpanContext: ${errorMessage(e)}`);
|
|
1412
1242
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1243
|
+
if (!isRecord(data)) throw new Error("Invalid LaminarSpanContext: must be a string or object");
|
|
1244
|
+
const traceId = data.traceId ?? data.trace_id;
|
|
1245
|
+
const spanId = data.spanId ?? data.span_id;
|
|
1246
|
+
const isRemote = data.isRemote ?? data.is_remote ?? false;
|
|
1247
|
+
const spanPath = data.spanPath ?? data.span_path;
|
|
1248
|
+
const spanIdsPath = data.spanIdsPath ?? data.span_ids_path;
|
|
1249
|
+
const userId = data.userId ?? data.user_id;
|
|
1250
|
+
const sessionId = data.sessionId ?? data.session_id;
|
|
1251
|
+
const metadata = data.metadata;
|
|
1252
|
+
const traceType = data.traceType ?? data.trace_type;
|
|
1253
|
+
const tracingLevel = data.tracingLevel ?? data.tracing_level;
|
|
1254
|
+
const debug = data.debug;
|
|
1255
|
+
if (typeof traceId !== "string" || typeof spanId !== "string") throw new Error("Invalid LaminarSpanContext: traceId and spanId must be strings");
|
|
1256
|
+
if (!isStringUUID(traceId) || !isStringUUID(spanId)) throw new Error("Invalid LaminarSpanContext: traceId and spanId must be valid UUIDs");
|
|
1257
|
+
return {
|
|
1258
|
+
traceId,
|
|
1259
|
+
spanId,
|
|
1260
|
+
isRemote: Boolean(isRemote),
|
|
1261
|
+
spanPath: Array.isArray(spanPath) ? spanPath : void 0,
|
|
1262
|
+
spanIdsPath: Array.isArray(spanIdsPath) ? spanIdsPath : void 0,
|
|
1263
|
+
userId,
|
|
1264
|
+
sessionId,
|
|
1265
|
+
metadata,
|
|
1266
|
+
traceType,
|
|
1267
|
+
tracingLevel,
|
|
1268
|
+
debug: isRecord(debug) ? deserializeDebugContext(debug) : void 0
|
|
1269
|
+
};
|
|
1270
|
+
};
|
|
1271
|
+
/**
|
|
1272
|
+
* Normalize a value to a canonical lowercase UUID string, or undefined.
|
|
1273
|
+
*
|
|
1274
|
+
* The debug block's `sessionId` / `replayTraceId` are always full ids; a value
|
|
1275
|
+
* that isn't UUID-shaped is dropped (treated as absent) rather than thrown, so
|
|
1276
|
+
* a partially-broken block never breaks span-context parsing.
|
|
1277
|
+
*/
|
|
1278
|
+
const asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1279
|
+
/**
|
|
1280
|
+
* Parse a debug block, accepting camelCase and snake_case. All ids are kept
|
|
1281
|
+
* VERBATIM: the producer emits the run's exact session / replay-trace /
|
|
1282
|
+
* cache-until strings (un-normalized — `LMNR_DEBUG_SESSION_ID` may be an
|
|
1283
|
+
* arbitrary non-UUID value), so the consumer must round-trip them unchanged or
|
|
1284
|
+
* a downstream run never joins the run. Keep line-comparable with the Python
|
|
1285
|
+
* `DebugContext.deserialize`.
|
|
1286
|
+
*/
|
|
1287
|
+
const deserializeDebugContext = (data) => ({
|
|
1288
|
+
enabled: data.enabled === true,
|
|
1289
|
+
sessionId: asString(data.sessionId ?? data.session_id),
|
|
1290
|
+
replayTraceId: asString(data.replayTraceId ?? data.replay_trace_id),
|
|
1291
|
+
cacheUntil: asString(data.cacheUntil ?? data.cache_until)
|
|
1292
|
+
});
|
|
1293
|
+
const getDirname = () => {
|
|
1294
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
1295
|
+
if (typeof import.meta.url !== "undefined") return path.dirname(fileURLToPath(import.meta.url));
|
|
1296
|
+
return process.cwd();
|
|
1297
|
+
};
|
|
1298
|
+
const MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 * 10;
|
|
1299
|
+
const TRUNCATION_SUFFIX = "...[Laminar: truncated]";
|
|
1300
|
+
/**
|
|
1301
|
+
* Cut an oversized span payload down to the limit rather than dropping it.
|
|
1302
|
+
*
|
|
1303
|
+
* Keeping the leading bytes preserves the start of the value, which is the useful part when
|
|
1304
|
+
* debugging. Mirrors `_truncate_payload` in the Python SDK.
|
|
1305
|
+
*
|
|
1306
|
+
* Callers pass the result of `JSON.stringify`, whose TS signature claims `string` but which
|
|
1307
|
+
* actually returns `undefined` for a top-level `undefined`, function, or symbol. Guard at
|
|
1308
|
+
* runtime and hand such values straight back: `setAttribute` treats them as a no-op, which is
|
|
1309
|
+
* the behaviour these payloads had before truncation existed.
|
|
1310
|
+
*/
|
|
1311
|
+
const truncateSpanPayload = (serialized, kind) => {
|
|
1312
|
+
if (typeof serialized !== "string") return serialized;
|
|
1313
|
+
if (serialized.length <= 10485760) return serialized;
|
|
1314
|
+
logger.warn(`Laminar: span ${kind} is ${serialized.length} bytes, which exceeds the ${MAX_MANUAL_SPAN_PAYLOAD_SIZE} byte limit. Truncating to the limit; the recorded value will not be valid JSON.`);
|
|
1315
|
+
const keep = MAX_MANUAL_SPAN_PAYLOAD_SIZE - 23;
|
|
1316
|
+
return serialized.slice(0, keep) + TRUNCATION_SUFFIX;
|
|
1317
|
+
};
|
|
1318
|
+
const isOtelAttributeValueType = (value) => {
|
|
1319
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
|
|
1320
|
+
if (Array.isArray(value)) {
|
|
1321
|
+
const allStrings = value.every((value) => value == null || typeof value === "string");
|
|
1322
|
+
const allNumbers = value.every((value) => value == null || typeof value === "number");
|
|
1323
|
+
const allBooleans = value.every((value) => value == null || typeof value === "boolean");
|
|
1324
|
+
return allStrings || allNumbers || allBooleans;
|
|
1415
1325
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1326
|
+
return false;
|
|
1327
|
+
};
|
|
1328
|
+
const metadataToAttributes = (metadata) => Object.fromEntries(Object.entries(metadata).map(([key, value]) => {
|
|
1329
|
+
if (isOtelAttributeValueType(value)) return [`${ASSOCIATION_PROPERTIES}.metadata.${key}`, value];
|
|
1330
|
+
else return [`${ASSOCIATION_PROPERTIES}.metadata.${key}`, JSON.stringify(value)];
|
|
1331
|
+
}));
|
|
1332
|
+
/**
|
|
1333
|
+
* Get OTEL environment variable with priority order.
|
|
1334
|
+
* Checks in order:
|
|
1335
|
+
* 1. OTEL_EXPORTER_OTLP_TRACES_{varName}
|
|
1336
|
+
* 2. OTEL_EXPORTER_OTLP_{varName}
|
|
1337
|
+
* 3. OTEL_{varName}
|
|
1338
|
+
*
|
|
1339
|
+
* @param varName - The variable name (e.g., 'ENDPOINT', 'HEADERS', 'PROTOCOL')
|
|
1340
|
+
* @returns The environment variable value or undefined if not found
|
|
1341
|
+
*/
|
|
1342
|
+
const getOtelEnvVar = (varName) => {
|
|
1343
|
+
const candidates = [
|
|
1344
|
+
`OTEL_EXPORTER_OTLP_TRACES_${varName}`,
|
|
1345
|
+
`OTEL_EXPORTER_OTLP_${varName}`,
|
|
1346
|
+
`OTEL_${varName}`
|
|
1347
|
+
];
|
|
1348
|
+
for (const candidate of candidates) {
|
|
1349
|
+
const value = process?.env?.[candidate];
|
|
1350
|
+
if (value) return value;
|
|
1418
1351
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1352
|
+
};
|
|
1353
|
+
/**
|
|
1354
|
+
* Check if OTEL configuration is available.
|
|
1355
|
+
* @returns true if OTEL endpoint is configured
|
|
1356
|
+
*/
|
|
1357
|
+
const hasOtelConfig = () => !!getOtelEnvVar("ENDPOINT");
|
|
1358
|
+
/**
|
|
1359
|
+
* Parse OTEL headers string into a record object.
|
|
1360
|
+
* Format: key1=value1,key2=value2
|
|
1361
|
+
* Values are URL-decoded.
|
|
1362
|
+
*
|
|
1363
|
+
* @param headersStr - Headers string in OTEL format
|
|
1364
|
+
* @returns Parsed headers object
|
|
1365
|
+
*/
|
|
1366
|
+
const parseOtelHeaders = (headersStr) => {
|
|
1367
|
+
if (!headersStr) return {};
|
|
1368
|
+
const headers = {};
|
|
1369
|
+
for (const pair of headersStr.split(",")) {
|
|
1370
|
+
const equalIndex = pair.indexOf("=");
|
|
1371
|
+
if (equalIndex !== -1) {
|
|
1372
|
+
const key = pair.substring(0, equalIndex).trim();
|
|
1373
|
+
const value = pair.substring(equalIndex + 1).trim();
|
|
1374
|
+
headers[key] = decodeURIComponent(value);
|
|
1375
|
+
}
|
|
1421
1376
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1377
|
+
return headers;
|
|
1378
|
+
};
|
|
1379
|
+
/**
|
|
1380
|
+
* Validate that either Laminar API key or OTEL configuration is present.
|
|
1381
|
+
* Throws an error if neither is configured.
|
|
1382
|
+
*
|
|
1383
|
+
* @param apiKey - The Laminar API key (if provided)
|
|
1384
|
+
* @throws Error if neither API key nor OTEL configuration is present
|
|
1385
|
+
*/
|
|
1386
|
+
const validateTracingConfig = (apiKey) => {
|
|
1387
|
+
if (!apiKey && !hasOtelConfig()) throw new Error("Please initialize the Laminar object with your project API key or set the LMNR_PROJECT_API_KEY environment variable, or configure OTEL environment variables (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, etc.)");
|
|
1388
|
+
};
|
|
1389
|
+
const loadEnv = (options) => {
|
|
1390
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
1391
|
+
const envDir = process.cwd();
|
|
1392
|
+
const envFiles = [
|
|
1393
|
+
".env",
|
|
1394
|
+
".env.local",
|
|
1395
|
+
`.env.${nodeEnv}`,
|
|
1396
|
+
`.env.${nodeEnv}.local`
|
|
1397
|
+
];
|
|
1398
|
+
const logLevel = process.env.LMNR_LOG_LEVEL ?? "info";
|
|
1399
|
+
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
1400
|
+
const quiet = options?.quiet ?? !verbose;
|
|
1401
|
+
config({
|
|
1402
|
+
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
1403
|
+
quiet
|
|
1404
|
+
});
|
|
1405
|
+
};
|
|
1406
|
+
/**
|
|
1407
|
+
* Converts an API base URL to the frontend/web URL.
|
|
1408
|
+
* - Converts https://api.lmnr.ai to https://www.laminar.sh
|
|
1409
|
+
* - Removes trailing slashes
|
|
1410
|
+
* - For localhost/127.0.0.1, ensures a port is specified (defaults to 5667)
|
|
1411
|
+
*
|
|
1412
|
+
* @param baseUrl - The API base URL (defaults to "https://api.lmnr.ai")
|
|
1413
|
+
* @returns The frontend URL
|
|
1414
|
+
*/
|
|
1415
|
+
const getFrontendUrl = (baseUrl, frontendPort) => {
|
|
1416
|
+
let url = baseUrl ?? "https://api.lmnr.ai";
|
|
1417
|
+
if (url === "https://api.lmnr.ai") url = "https://www.laminar.sh";
|
|
1418
|
+
url = url.replace(/\/$/, "");
|
|
1419
|
+
if (/localhost|127\.0\.0\.1/.test(url)) {
|
|
1420
|
+
const port = frontendPort ?? url.match(/:\d{1,5}$/g)?.[0]?.slice(1) ?? 5667;
|
|
1421
|
+
if (/:(\d{1,5})$/.test(url)) url = url.replace(/:\d{1,5}$/g, `:${port}`);
|
|
1422
|
+
else url = `${url}:${port}`;
|
|
1424
1423
|
}
|
|
1424
|
+
return url;
|
|
1425
1425
|
};
|
|
1426
1426
|
//#endregion
|
|
1427
|
-
export {
|
|
1427
|
+
export { SPAN_INSTRUMENTATION_SOURCE as A, LaminarClient as B, PARENT_SPAN_IDS_PATH as C, SPAN_INPUT as D, SPAN_IDS_PATH as E, SPAN_TYPE as F, DEBUG_SESSION_FILE as H, TRACE_HAS_BROWSER_SESSION as I, TRACE_TYPE as L, SPAN_OUTPUT as M, SPAN_PATH as N, SPAN_INSTRUMENTATION_SCOPE_NAME as O, SPAN_SDK_VERSION as P, USER_ID as R, LaminarAttributes as S, SESSION_ID as T, TracingLevel as U, DEBUG_SESSION_DIR as V, errorMessage as W, tryToOtelSpanContext as _, getFrontendUrl as a, ASSOCIATION_PROPERTIES_OVERRIDES as b, loadEnv as c, normalizeOtelSpanId as d, normalizeOtelTraceId as f, truncateSpanPayload as g, parseOtelHeaders as h, getDirname as i, SPAN_LANGUAGE_VERSION as j, SPAN_INSTRUMENTATION_SCOPE_VERSION as k, metadataToAttributes as l, otelTraceIdToUUID as m, Semaphore as n, getOtelEnvVar as o, otelSpanIdToUUID as p, deserializeLaminarSpanContext as r, initializeLogger as s, NIL_UUID as t, newUUID as u, validateTracingConfig as v, PARENT_SPAN_PATH as w, HUMAN_EVALUATOR_OPTIONS as x, ASSOCIATION_PROPERTIES as y, version as z };
|
|
1428
1428
|
|
|
1429
|
-
//# sourceMappingURL=
|
|
1429
|
+
//# sourceMappingURL=utils-UmlDPMxe.mjs.map
|