@oxygen-agent/cli 1.1003.12 → 1.1010.644
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/README.md +1 -1
- package/dist/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.js +15 -2
- package/dist/functions-commands.js +11 -11
- package/dist/index.js +1222 -159
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/skills.js +34 -10
- package/dist/util.d.ts +9 -0
- package/dist/util.js +14 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/billing.js +45 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +223 -13
- package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +5 -23
- package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +9 -4
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +11 -8
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +4 -4
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +4 -4
- package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +13 -23
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +407 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +29 -4
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +189 -36
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +21 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +21 -1
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +5 -1
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +19 -1
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
- package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.js +8 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
- package/node_modules/@oxygen/shared/package.json +9 -0
- package/package.json +2 -1
|
@@ -36,6 +36,8 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
36
36
|
import { OXYGEN_VERSION } from "./version.js";
|
|
37
37
|
import { prepareLlmPayload } from "./llm-payload.js";
|
|
38
38
|
import { log } from "./log.js";
|
|
39
|
+
import { llmPromptLabel, registeredLlmPrompt } from "./llm-prompts.js";
|
|
40
|
+
import { activeTelemetryTraceId } from "./telemetry.js";
|
|
39
41
|
const FLUSH_TIMEOUT_MS = 5_000;
|
|
40
42
|
const WARN_THROTTLE_MS = 30_000;
|
|
41
43
|
/**
|
|
@@ -99,6 +101,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
|
|
|
99
101
|
* the web app nor the worker.
|
|
100
102
|
*/
|
|
101
103
|
export function resolveLlmTracingServiceName(env = process.env) {
|
|
104
|
+
const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
|
|
105
|
+
if (deployEnv === "selfhost-dev" || deployEnv === "selfhost-prod" || deployEnv === "selfhost-shadow") {
|
|
106
|
+
if (env.OXYGEN_PROCESS_ROLE === "worker") {
|
|
107
|
+
return deployEnv === "selfhost-prod" ? "oxygen-worker" : "oxygen-worker-dev";
|
|
108
|
+
}
|
|
109
|
+
if (env.OXYGEN_PROCESS_ROLE === "web" || env.OXYGEN_SERVICE_NAME === "oxygen-web")
|
|
110
|
+
return "oxygen-web";
|
|
111
|
+
return "oxygen-local";
|
|
112
|
+
}
|
|
102
113
|
const flyApp = env.FLY_APP_NAME?.trim() || env.FLY_APP?.trim();
|
|
103
114
|
if (flyApp)
|
|
104
115
|
return flyApp.includes("-dev") ? "oxygen-worker-dev" : "oxygen-worker";
|
|
@@ -128,6 +139,9 @@ export function llmTraceIdForSeed(seed) {
|
|
|
128
139
|
function rootSpanIdForSeed(seed) {
|
|
129
140
|
return createHash("sha256").update(`langfuse-root:${seed}`, "utf8").digest("hex").slice(0, 16);
|
|
130
141
|
}
|
|
142
|
+
function observationSpanId(parts) {
|
|
143
|
+
return createHash("sha256").update(JSON.stringify(parts), "utf8").digest("hex").slice(0, 16);
|
|
144
|
+
}
|
|
131
145
|
// A score's `comment` is free text (a thumbs-down reason is user-authored and
|
|
132
146
|
// unbounded) and the API types it as a STRING, so it cannot take
|
|
133
147
|
// a recoverable payload envelope. It gets the same
|
|
@@ -148,9 +162,57 @@ function compact(body) {
|
|
|
148
162
|
}
|
|
149
163
|
return out;
|
|
150
164
|
}
|
|
165
|
+
// --- Export summary ----------------------------------------------------------
|
|
166
|
+
// ingest_failed is throttled per process, so it is a floor on failures and says
|
|
167
|
+
// nothing about the healthy volume beside it. This rollup gives the ratio: how
|
|
168
|
+
// many spans Langfuse acknowledged, how many batches it refused, and how many
|
|
169
|
+
// observations never reached the exporter at all. Counts only; never content.
|
|
170
|
+
const EXPORT_SUMMARY_INTERVAL_MS = 60_000;
|
|
171
|
+
export function createLlmExportSummary(now = Date.now) {
|
|
172
|
+
let exported = 0;
|
|
173
|
+
let failed = 0;
|
|
174
|
+
let dropped = 0;
|
|
175
|
+
let windowStartedAt = now();
|
|
176
|
+
const report = () => {
|
|
177
|
+
const at = now();
|
|
178
|
+
if (exported + failed + dropped > 0) {
|
|
179
|
+
log("info", "llm_tracing.export_summary", {
|
|
180
|
+
provider: "langfuse",
|
|
181
|
+
exported,
|
|
182
|
+
failed,
|
|
183
|
+
dropped,
|
|
184
|
+
window_seconds: Math.max(0, Math.round((at - windowStartedAt) / 1000)),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
exported = 0;
|
|
188
|
+
failed = 0;
|
|
189
|
+
dropped = 0;
|
|
190
|
+
windowStartedAt = at;
|
|
191
|
+
};
|
|
192
|
+
const add = (count) => (Number.isFinite(count) && count > 0 ? Math.floor(count) : 0);
|
|
193
|
+
return {
|
|
194
|
+
exported: (count) => { exported += add(count); },
|
|
195
|
+
failed: (count) => { failed += add(count); },
|
|
196
|
+
dropped: (count) => { dropped += add(count); },
|
|
197
|
+
maybeReport: () => { if (now() - windowStartedAt >= EXPORT_SUMMARY_INTERVAL_MS)
|
|
198
|
+
report(); },
|
|
199
|
+
reportNow: report,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A thrown value's class name is the only part of it that may leave the
|
|
204
|
+
* private store: messages from payload preparation can quote the payload.
|
|
205
|
+
*/
|
|
206
|
+
function sanitizedErrorReason(error) {
|
|
207
|
+
const name = error instanceof Error ? error.name : typeof error;
|
|
208
|
+
return /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ? name : "unknown";
|
|
209
|
+
}
|
|
151
210
|
function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
|
|
152
211
|
return new Promise((resolve) => {
|
|
153
|
-
const timer = setTimeout(
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
warn(new Error("langfuse_flush_deadline_exceeded"));
|
|
214
|
+
resolve();
|
|
215
|
+
}, timeoutMs);
|
|
154
216
|
timer.unref?.();
|
|
155
217
|
rejectable
|
|
156
218
|
.catch((error) => warn(error))
|
|
@@ -161,17 +223,133 @@ function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
|
|
|
161
223
|
});
|
|
162
224
|
}
|
|
163
225
|
/** Lazy, private, always-sampled OTel transport. Never register it globally. */
|
|
164
|
-
function createOtelEmitter(env, warn) {
|
|
226
|
+
function createOtelEmitter(env, warn, summary) {
|
|
165
227
|
let handle = null;
|
|
166
228
|
const init = () => {
|
|
167
229
|
handle ??= (async () => {
|
|
168
230
|
try {
|
|
169
|
-
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core] = await Promise.all([
|
|
231
|
+
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core, { JsonTraceSerializer }] = await Promise.all([
|
|
170
232
|
import("@langfuse/otel"), import("@opentelemetry/sdk-trace-base"),
|
|
171
233
|
import("@opentelemetry/resources"),
|
|
172
234
|
import("@langfuse/tracing"), import("@opentelemetry/api"), import("@langfuse/core"),
|
|
235
|
+
import("@opentelemetry/otlp-transformer"),
|
|
173
236
|
]);
|
|
237
|
+
const exportUrl = `${(env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "")}/api/public/otel/v1/traces`;
|
|
238
|
+
const exportHeaders = {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
Authorization: `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`,
|
|
241
|
+
"x-langfuse-sdk-name": "javascript",
|
|
242
|
+
"x-langfuse-sdk-version": core.LANGFUSE_SDK_VERSION,
|
|
243
|
+
"x-langfuse-public-key": env.LANGFUSE_PUBLIC_KEY,
|
|
244
|
+
};
|
|
245
|
+
const pendingExports = new Set();
|
|
246
|
+
// The upstream HTTP exporter logs raw partial-success/malformed response
|
|
247
|
+
// bodies through GLOBAL diagnostics before its callback runs. Keep the
|
|
248
|
+
// supported OTLP JSON serializer, but own delivery and response handling
|
|
249
|
+
// inside this private provider. Never mutate the global diagnostic logger.
|
|
250
|
+
const send = async (spans) => {
|
|
251
|
+
const encoded = JsonTraceSerializer.serializeRequest(spans);
|
|
252
|
+
if (!encoded)
|
|
253
|
+
throw new Error("serialization_failed");
|
|
254
|
+
const body = new TextDecoder().decode(encoded);
|
|
255
|
+
const signal = AbortSignal.timeout(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000);
|
|
256
|
+
let response;
|
|
257
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
if (signal.aborted)
|
|
263
|
+
throw new Error("deadline_exceeded");
|
|
264
|
+
if (attempt === 2)
|
|
265
|
+
throw new Error("network_failed");
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (![429, 502, 503, 504].includes(response.status) || attempt === 2)
|
|
270
|
+
break;
|
|
271
|
+
// A retryable rejection has no useful private response to retain.
|
|
272
|
+
await response.body?.cancel().catch(() => { });
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
274
|
+
}
|
|
275
|
+
if (!response?.ok) {
|
|
276
|
+
await response?.body?.cancel().catch(() => { });
|
|
277
|
+
throw new Error("http_rejected");
|
|
278
|
+
}
|
|
279
|
+
const chunks = [];
|
|
280
|
+
let bytes = 0;
|
|
281
|
+
const reader = response.body?.getReader();
|
|
282
|
+
if (reader) {
|
|
283
|
+
try {
|
|
284
|
+
for (;;) {
|
|
285
|
+
const next = await reader.read();
|
|
286
|
+
if (next.done)
|
|
287
|
+
break;
|
|
288
|
+
bytes += next.value.byteLength;
|
|
289
|
+
// OTLP acknowledgements are tiny; an echoed payload must never
|
|
290
|
+
// become an unbounded allocation or reach any diagnostic logger.
|
|
291
|
+
if (bytes > 64 * 1024)
|
|
292
|
+
throw new Error("response_too_large");
|
|
293
|
+
chunks.push(next.value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
await reader.cancel().catch(() => { });
|
|
298
|
+
throw new Error(error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
reader.releaseLock();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let acknowledgement;
|
|
305
|
+
try {
|
|
306
|
+
acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
throw new Error("response_invalid");
|
|
310
|
+
}
|
|
311
|
+
if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
|
|
312
|
+
throw new Error("response_invalid");
|
|
313
|
+
const envelope = acknowledgement;
|
|
314
|
+
const partial = envelope.partialSuccess ?? envelope.partial_success;
|
|
315
|
+
if (partial !== undefined && partial !== null) {
|
|
316
|
+
if (typeof partial !== "object" || Array.isArray(partial))
|
|
317
|
+
throw new Error("response_invalid");
|
|
318
|
+
const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
|
|
319
|
+
if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
|
|
320
|
+
throw new Error("response_invalid");
|
|
321
|
+
if (Number(rawRejected) > 0)
|
|
322
|
+
throw new Error("partial_rejection");
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const privateExporter = {
|
|
326
|
+
export: (spans, done) => {
|
|
327
|
+
const pending = send(spans).then(() => {
|
|
328
|
+
// Complete the batch first: a logging fault must never leave the
|
|
329
|
+
// processor waiting out its own export timeout.
|
|
330
|
+
done({ code: 0 });
|
|
331
|
+
summary.exported(spans.length);
|
|
332
|
+
summary.maybeReport();
|
|
333
|
+
}, (error) => {
|
|
334
|
+
const failures = new Set(["serialization_failed", "deadline_exceeded", "network_failed", "http_rejected", "response_too_large", "response_read_failed", "response_invalid", "partial_rejection"]);
|
|
335
|
+
const failureKind = error instanceof Error && failures.has(error.message) ? error.message : "transport_failed";
|
|
336
|
+
// BSP forwards failures to the global OTel error handler. It only
|
|
337
|
+
// receives our constant classification, never the server body.
|
|
338
|
+
done({ code: 1, error: new Error("langfuse_delivery_failed") });
|
|
339
|
+
summary.failed(spans.length);
|
|
340
|
+
summary.maybeReport();
|
|
341
|
+
warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind });
|
|
342
|
+
});
|
|
343
|
+
pendingExports.add(pending);
|
|
344
|
+
// Even an unexpected SDK callback failure must not create a rejected
|
|
345
|
+
// fire-and-forget promise in the product process.
|
|
346
|
+
void pending.then(() => pendingExports.delete(pending), () => pendingExports.delete(pending));
|
|
347
|
+
},
|
|
348
|
+
shutdown: async () => { await Promise.allSettled([...pendingExports]); },
|
|
349
|
+
forceFlush: async () => { await Promise.allSettled([...pendingExports]); },
|
|
350
|
+
};
|
|
174
351
|
const processor = new LangfuseSpanProcessor({
|
|
352
|
+
exporter: privateExporter,
|
|
175
353
|
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
|
176
354
|
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
177
355
|
...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
|
|
@@ -267,7 +445,19 @@ function createOtelEmitter(env, warn) {
|
|
|
267
445
|
return handle;
|
|
268
446
|
};
|
|
269
447
|
return {
|
|
270
|
-
emit: (emission) => {
|
|
448
|
+
emit: (emission) => {
|
|
449
|
+
void init().then((h) => {
|
|
450
|
+
// init already warned once; every later emission is lost the same way.
|
|
451
|
+
// Report from here too: a process whose transport never came up has
|
|
452
|
+
// no exports to carry the window, and that is when dropped matters.
|
|
453
|
+
if (!h) {
|
|
454
|
+
summary.dropped(1);
|
|
455
|
+
summary.maybeReport();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
h.emit(emission);
|
|
459
|
+
}).catch((error) => { summary.dropped(1); summary.maybeReport(); warn(error, { stage: emission.kind }); });
|
|
460
|
+
},
|
|
271
461
|
flush: () => init().then((h) => h?.processor.forceFlush() ?? Promise.resolve()),
|
|
272
462
|
shutdown: () => init().then((h) => h?.processor.shutdown() ?? Promise.resolve()),
|
|
273
463
|
};
|
|
@@ -347,6 +537,126 @@ function createApiScorer(env, warn) {
|
|
|
347
537
|
},
|
|
348
538
|
};
|
|
349
539
|
}
|
|
540
|
+
const NOOP_PROMPT_RESOLVER = { versionFor: () => undefined };
|
|
541
|
+
const PROMPT_FOUND_TTL_MS = 24 * 60 * 60 * 1000;
|
|
542
|
+
const PROMPT_MISSING_TTL_MS = 10 * 60 * 1000;
|
|
543
|
+
const PROMPT_ERROR_TTL_MS = 60 * 1000;
|
|
544
|
+
const PROMPT_CACHE_LIMIT = 512;
|
|
545
|
+
function promptMetadata(prompt) {
|
|
546
|
+
return prompt
|
|
547
|
+
? { oxygen_prompt_name: prompt.name, oxygen_prompt_hash: prompt.hash, oxygen_prompt_registered: prompt.registered }
|
|
548
|
+
: {};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Only a deployed dev process talking to the self-hosted instance may create
|
|
552
|
+
* prompt versions. Production and shadow data never do (the environment label
|
|
553
|
+
* is "production" there, or the host is Langfuse Cloud), and a local process
|
|
554
|
+
* would publish whatever uncommitted template its checkout holds.
|
|
555
|
+
*/
|
|
556
|
+
function mayPublishMissingPrompts(env, baseUrl) {
|
|
557
|
+
if (resolveLlmTracingEnvironment(env) !== "development")
|
|
558
|
+
return false;
|
|
559
|
+
if (resolveLlmTracingServiceName(env) === "oxygen-local")
|
|
560
|
+
return false;
|
|
561
|
+
if (env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase() !== "selfhost-dev")
|
|
562
|
+
return false;
|
|
563
|
+
let host;
|
|
564
|
+
try {
|
|
565
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
return host !== "langfuse.com" && !host.endsWith(".langfuse.com");
|
|
571
|
+
}
|
|
572
|
+
export function createApiPromptResolver(env, warn, fetchImpl = fetch, now = Date.now) {
|
|
573
|
+
const baseUrl = (env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "");
|
|
574
|
+
const authorization = `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`;
|
|
575
|
+
const cache = new Map();
|
|
576
|
+
const inflight = new Set();
|
|
577
|
+
const publishAttempted = new Set();
|
|
578
|
+
const publishes = mayPublishMissingPrompts(env, baseUrl);
|
|
579
|
+
// One attempt per template per process; any failure falls back to metadata only.
|
|
580
|
+
const publish = async (key, prompt) => {
|
|
581
|
+
const definition = publishes && !publishAttempted.has(key) ? registeredLlmPrompt(prompt) : undefined;
|
|
582
|
+
if (!definition)
|
|
583
|
+
return undefined;
|
|
584
|
+
publishAttempted.add(key);
|
|
585
|
+
const label = llmPromptLabel(prompt.hash);
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts`, {
|
|
588
|
+
method: "POST",
|
|
589
|
+
headers: { Authorization: authorization, "Content-Type": "application/json" },
|
|
590
|
+
body: JSON.stringify({
|
|
591
|
+
type: "text", name: definition.name, prompt: definition.template, labels: [label],
|
|
592
|
+
tags: ["oxygen", "code-owned"], commitMessage: `oxygen code template ${prompt.hash} (runtime publish)`,
|
|
593
|
+
}),
|
|
594
|
+
signal: AbortSignal.timeout(5_000),
|
|
595
|
+
redirect: "error",
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
await response.body?.cancel().catch(() => { });
|
|
599
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish", status: response.status });
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
const body = await response.json();
|
|
603
|
+
const labels = Array.isArray(body.labels) ? body.labels : [];
|
|
604
|
+
return typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label) ? body.version : undefined;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish" });
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const lookup = async (key, prompt) => {
|
|
612
|
+
const label = llmPromptLabel(prompt.hash);
|
|
613
|
+
let entry = { version: undefined, expiresAt: now() + PROMPT_ERROR_TTL_MS };
|
|
614
|
+
try {
|
|
615
|
+
const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts/${encodeURIComponent(prompt.name)}?label=${encodeURIComponent(label)}`, { headers: { Authorization: authorization }, signal: AbortSignal.timeout(5_000), redirect: "error" });
|
|
616
|
+
if (response.status === 404) {
|
|
617
|
+
await response.body?.cancel().catch(() => { });
|
|
618
|
+
const version = await publish(key, prompt);
|
|
619
|
+
entry = version === undefined
|
|
620
|
+
? { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS }
|
|
621
|
+
: { version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
|
|
622
|
+
}
|
|
623
|
+
else if (!response.ok) {
|
|
624
|
+
await response.body?.cancel().catch(() => { });
|
|
625
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link", status: response.status });
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const body = await response.json();
|
|
629
|
+
const labels = Array.isArray(body.labels) ? body.labels : [];
|
|
630
|
+
if (typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label)) {
|
|
631
|
+
entry = { version: body.version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
entry = { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link" });
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
if (cache.size >= PROMPT_CACHE_LIMIT)
|
|
643
|
+
cache.delete(cache.keys().next().value);
|
|
644
|
+
cache.set(key, entry);
|
|
645
|
+
inflight.delete(key);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
return {
|
|
649
|
+
versionFor: (prompt) => {
|
|
650
|
+
const key = `${prompt.name}\u0000${prompt.hash}`;
|
|
651
|
+
const cached = cache.get(key);
|
|
652
|
+
if ((!cached || cached.expiresAt <= now()) && !inflight.has(key)) {
|
|
653
|
+
inflight.add(key);
|
|
654
|
+
void lookup(key, prompt);
|
|
655
|
+
}
|
|
656
|
+
return cached?.version;
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
350
660
|
/**
|
|
351
661
|
* Construct a fail-open Langfuse client, or `null` when tracing is disabled or
|
|
352
662
|
* misconfigured. Prefer the process-wide `getLlmTracingClient` in app code;
|
|
@@ -355,24 +665,65 @@ function createApiScorer(env, warn) {
|
|
|
355
665
|
export function createLlmTracingClient(env = process.env, options) {
|
|
356
666
|
if (!options?.emitterImpl && !isLlmTracingEnabled(env))
|
|
357
667
|
return null;
|
|
358
|
-
|
|
668
|
+
const lastWarnAtMs = new Map();
|
|
359
669
|
const warn = (error, context) => {
|
|
360
670
|
const nowMs = Date.now();
|
|
361
|
-
|
|
671
|
+
const errorCode = error instanceof Error && ["langfuse_flush_deadline_exceeded", "langfuse_replay_identity_unavailable", "langfuse_prompt_link_failed", "langfuse_observation_build_failed"].includes(error.message)
|
|
672
|
+
? error.message : "langfuse_delivery_failed";
|
|
673
|
+
// An identity gap must not suppress a later actual delivery failure.
|
|
674
|
+
const warningKey = `${errorCode}:${context?.stage ?? "unknown"}`;
|
|
675
|
+
if (nowMs - (lastWarnAtMs.get(warningKey) ?? -Infinity) < WARN_THROTTLE_MS)
|
|
362
676
|
return;
|
|
363
|
-
lastWarnAtMs
|
|
677
|
+
lastWarnAtMs.set(warningKey, nowMs);
|
|
364
678
|
log("warn", "llm_tracing.ingest_failed", {
|
|
365
679
|
provider: "langfuse",
|
|
366
|
-
|
|
680
|
+
// Exporter/API error messages can contain the rejected request or server
|
|
681
|
+
// response. Only this fixed classification may leave the private store.
|
|
682
|
+
error_code: errorCode,
|
|
367
683
|
...context,
|
|
368
684
|
});
|
|
369
685
|
};
|
|
370
|
-
|
|
686
|
+
// Opt-in per environment; managed projects retain their existing append
|
|
687
|
+
// semantics. A real execution is identified by the caller's immutable start
|
|
688
|
+
// plus logical id, never by the logical id alone (approval/tool ids can recur).
|
|
689
|
+
const stableIdentity = env.LANGFUSE_OBSERVATION_IDENTITY === "stable-v1";
|
|
690
|
+
const physicalId = (body, kind) => {
|
|
691
|
+
if (!stableIdentity)
|
|
692
|
+
return undefined;
|
|
693
|
+
// Verified ledger-backed control hooks supply the exact durable dedupe key.
|
|
694
|
+
// It already defines identity in the authoritative session/run ledger, so
|
|
695
|
+
// identity hashing needs no additional clock. The durable capture adapter
|
|
696
|
+
// separately supplies the original ledger timestamp for v3's date key.
|
|
697
|
+
const durableEventKey = body.metadata?.oxygen_durable_event_key;
|
|
698
|
+
if (kind === "event" && typeof durableEventKey === "string" && durableEventKey.length > 0) {
|
|
699
|
+
return observationSpanId(["langfuse-durable-event-v1", body.traceId, body.name, durableEventKey]);
|
|
700
|
+
}
|
|
701
|
+
// Approval ids name a durable decision/request across invocation slices.
|
|
702
|
+
// Other event keys are not universally unique; do not infer that guarantee.
|
|
703
|
+
if (kind === "event" && /^apr:(requested|decided):.+/.test(body.id)) {
|
|
704
|
+
return observationSpanId(["langfuse-event-v1", body.traceId, body.name, body.id]);
|
|
705
|
+
}
|
|
706
|
+
if (!body.startTime || !Number.isFinite(body.startTime.getTime())) {
|
|
707
|
+
warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: kind });
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
return observationSpanId(["langfuse-observation-v1", body.traceId, kind, body.name, body.id,
|
|
711
|
+
body.startTime.toISOString(), body.metadata?.attempt_id ?? null, body.metadata?.call_id ?? null]);
|
|
712
|
+
};
|
|
713
|
+
const summary = createLlmExportSummary();
|
|
714
|
+
const transport = options?.emitterImpl ?? createOtelEmitter(env, warn, summary);
|
|
371
715
|
const emitter = {
|
|
372
716
|
...transport,
|
|
373
717
|
emit: (emission) => {
|
|
374
718
|
const observationId = emission.observationId ?? randomBytes(8).toString("hex");
|
|
375
719
|
const metadata = { ...emission.attributes.metadata, oxygen_capture_version: 2 };
|
|
720
|
+
// Cross-link to the operational trace (SigNoz/Axiom). Read here, still in
|
|
721
|
+
// the caller's synchronous context; the private provider never sees it.
|
|
722
|
+
if (metadata.otel_trace_id === undefined) {
|
|
723
|
+
const otelTraceId = activeTelemetryTraceId();
|
|
724
|
+
if (otelTraceId)
|
|
725
|
+
metadata.otel_trace_id = otelTraceId;
|
|
726
|
+
}
|
|
376
727
|
const attributes = { ...emission.attributes, version: OXYGEN_VERSION, metadata };
|
|
377
728
|
const chunkEmissions = [];
|
|
378
729
|
const capture = (value, field, key) => {
|
|
@@ -380,12 +731,18 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
380
731
|
for (const chunk of prepared.chunks) {
|
|
381
732
|
chunkEmissions.push({
|
|
382
733
|
kind: "event", traceSeed: emission.traceSeed, name: "llm.payload_chunk", parentObservationId: observationId,
|
|
734
|
+
...(stableIdentity ? { observationId: observationSpanId(["langfuse-payload-v1", observationId, field, chunk.payload_id, chunk.index]) } : {}),
|
|
383
735
|
attributes: { version: OXYGEN_VERSION, input: chunk, metadata: {
|
|
384
736
|
oxygen_capture_version: 2, oxygen_payload_id: chunk.payload_id,
|
|
385
737
|
oxygen_payload_chunk_index: chunk.index, oxygen_payload_chunk_count: prepared.chunks.length,
|
|
386
738
|
oxygen_observation_id: `${metadata.oxygen_observation_id ?? observationId}:payload:${field}:${chunk.index}`,
|
|
387
739
|
} },
|
|
388
|
-
|
|
740
|
+
// V3's replacing key includes the start date. The root's end can
|
|
741
|
+
// move on resume, while unchanged chunks retain their IDs; pin their
|
|
742
|
+
// date to the immutable owner start in stable mode as well.
|
|
743
|
+
correlation: emission.correlation,
|
|
744
|
+
startTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
|
|
745
|
+
endTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
|
|
389
746
|
});
|
|
390
747
|
}
|
|
391
748
|
return prepared.value;
|
|
@@ -414,12 +771,18 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
414
771
|
},
|
|
415
772
|
};
|
|
416
773
|
const scorer = options?.scorerImpl ?? createApiScorer(env, warn);
|
|
774
|
+
const promptResolver = options?.promptResolverImpl ?? (options?.emitterImpl ? NOOP_PROMPT_RESOLVER : createApiPromptResolver(env, warn));
|
|
417
775
|
const guarded = (fn, stage) => {
|
|
418
776
|
try {
|
|
419
777
|
fn();
|
|
420
778
|
}
|
|
421
779
|
catch (error) {
|
|
422
|
-
|
|
780
|
+
// Building the observation failed before any transport saw it (payload
|
|
781
|
+
// preparation, identity hashing, a substituted emitter). That is a lost
|
|
782
|
+
// capture, not a delivery failure, and must not read as one.
|
|
783
|
+
summary.dropped(1);
|
|
784
|
+
summary.maybeReport();
|
|
785
|
+
warn(new Error("langfuse_observation_build_failed"), { stage, reason: sanitizedErrorReason(error) });
|
|
423
786
|
}
|
|
424
787
|
};
|
|
425
788
|
// propagateAttributes rejects non-string ids and anything over 200 chars, and
|
|
@@ -442,11 +805,17 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
442
805
|
tags: input.tags,
|
|
443
806
|
});
|
|
444
807
|
};
|
|
808
|
+
const promptLink = (prompt) => {
|
|
809
|
+
if (!prompt?.registered)
|
|
810
|
+
return {};
|
|
811
|
+
const version = promptResolver.versionFor(prompt);
|
|
812
|
+
return version === undefined ? {} : { prompt: { name: prompt.name, version, isFallback: false } };
|
|
813
|
+
};
|
|
445
814
|
const modelObservation = (body, kind) => guarded(() => {
|
|
446
815
|
emitter.emit({
|
|
447
816
|
kind,
|
|
448
817
|
traceSeed: body.traceId,
|
|
449
|
-
|
|
818
|
+
observationId: body.observationId ?? physicalId(body, kind),
|
|
450
819
|
name: body.name,
|
|
451
820
|
attributes: compact({
|
|
452
821
|
input: body.input,
|
|
@@ -458,7 +827,8 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
458
827
|
completionStartTime: body.completionStartTime,
|
|
459
828
|
usageDetails: body.usageDetails,
|
|
460
829
|
costDetails: body.costDetails,
|
|
461
|
-
|
|
830
|
+
...promptLink(body.prompt),
|
|
831
|
+
metadata: { ...(body.metadata ?? {}), ...promptMetadata(body.prompt), oxygen_observation_id: body.id },
|
|
462
832
|
}),
|
|
463
833
|
// The cost-bearing observation: v5 session cost only rolls up when
|
|
464
834
|
// the generation itself carries the session.
|
|
@@ -495,6 +865,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
495
865
|
const sliceStart = typeof body.metadata.slice_started_at === "string" ? new Date(body.metadata.slice_started_at) : startTime;
|
|
496
866
|
emitter.emit({
|
|
497
867
|
kind: "span", traceSeed: body.id, name: `${body.name}.slice`,
|
|
868
|
+
...(stableIdentity ? { observationId: observationSpanId(["langfuse-slice-v1", body.id, body.metadata.slice_id, sliceStart.getTime()]) } : {}),
|
|
498
869
|
attributes: compact({ input: body.input, output: body.output, metadata: { ...body.metadata, oxygen_observation_id: `${body.id}:slice:${body.metadata.slice_id}` } }),
|
|
499
870
|
correlation: correlate({ sessionId: body.sessionId, userId: body.userId }),
|
|
500
871
|
startTime: Number.isFinite(sliceStart.getTime()) ? sliceStart : startTime, endTime: body.endTime ?? startTime,
|
|
@@ -505,6 +876,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
505
876
|
emitter.emit({
|
|
506
877
|
kind: "span",
|
|
507
878
|
traceSeed: body.traceId,
|
|
879
|
+
observationId: physicalId(body, "span"),
|
|
508
880
|
name: body.name,
|
|
509
881
|
attributes: compact({
|
|
510
882
|
input: body.input,
|
|
@@ -521,10 +893,22 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
521
893
|
generation: (body) => modelObservation(body, "generation"),
|
|
522
894
|
embedding: (body) => modelObservation(body, "embedding"),
|
|
523
895
|
event: (body) => guarded(() => {
|
|
896
|
+
const durableKey = body.metadata?.oxygen_durable_event_key;
|
|
897
|
+
const durableEvent = (typeof durableKey === "string" && durableKey.length > 0) || /^apr:(requested|decided):.+/.test(body.id);
|
|
898
|
+
if (stableIdentity && durableEvent && (!body.startTime || !Number.isFinite(body.startTime.getTime()))) {
|
|
899
|
+
// A stable id with "now" as its date creates another V3 observation
|
|
900
|
+
// after midnight. Missing canonical time is missing capture, not a
|
|
901
|
+
// reason to fabricate a historical event or duplicate it silently.
|
|
902
|
+
warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: "event" });
|
|
903
|
+
summary.dropped(1);
|
|
904
|
+
summary.maybeReport();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
524
907
|
const startTime = body.startTime ?? new Date();
|
|
525
908
|
emitter.emit({
|
|
526
909
|
kind: "event",
|
|
527
910
|
traceSeed: body.traceId,
|
|
911
|
+
observationId: physicalId(body, "event"),
|
|
528
912
|
name: body.name,
|
|
529
913
|
attributes: compact({
|
|
530
914
|
input: body.input,
|
|
@@ -551,7 +935,14 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
551
935
|
// enforced here (15s for serverless after()) rather than inside one emitter — an emitter that throws
|
|
552
936
|
// synchronously or rejects must still not escape into product code.
|
|
553
937
|
flush: () => boundedNever((async () => emitter.flush())(), (error) => warn(error, { stage: "flush" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
|
|
554
|
-
shutdown: () => boundedNever(
|
|
938
|
+
shutdown: () => boundedNever(
|
|
939
|
+
// The final window is reported whatever its age: nothing follows it.
|
|
940
|
+
(async () => { try {
|
|
941
|
+
await emitter.shutdown();
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
summary.reportNow();
|
|
945
|
+
} })(), (error) => warn(error, { stage: "shutdown" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
|
|
555
946
|
};
|
|
556
947
|
}
|
|
557
948
|
// --- Process-wide singleton (both runtimes construct at most one client) ------
|
|
@@ -563,6 +954,8 @@ function cacheKey(env) {
|
|
|
563
954
|
env.LANGFUSE_SECRET_KEY ?? "",
|
|
564
955
|
env.LANGFUSE_BASE_URL ?? "",
|
|
565
956
|
resolveLlmTracingEnvironment(env),
|
|
957
|
+
resolveLlmTracingServiceName(env),
|
|
958
|
+
env.LANGFUSE_OBSERVATION_IDENTITY ?? "",
|
|
566
959
|
].join("|");
|
|
567
960
|
}
|
|
568
961
|
export function getLlmTracingClient(env = process.env) {
|
|
@@ -30,3 +30,4 @@ export declare const LINKEDIN_COUNTRIES: ReadonlyArray<readonly [code: string, n
|
|
|
30
30
|
* honest outcome, and the raw payload is still on the record.
|
|
31
31
|
*/
|
|
32
32
|
export declare function resolveLinkedInCountry(value: unknown): string | null;
|
|
33
|
+
export { LINKEDIN_COUNTRY_PRIMARY_TIME_ZONES, linkedInCountryPrimaryTimeZone, } from "./linkedin-country-timezones.js";
|
|
@@ -357,3 +357,5 @@ export function resolveLinkedInCountry(value) {
|
|
|
357
357
|
const aliased = COUNTRY_ALIASES[lower];
|
|
358
358
|
return aliased ? BY_CODE.get(aliased) ?? null : null;
|
|
359
359
|
}
|
|
360
|
+
// The account-owner country also picks a LinkedIn sender's default working-hours zone.
|
|
361
|
+
export { LINKEDIN_COUNTRY_PRIMARY_TIME_ZONES, linkedInCountryPrimaryTimeZone, } from "./linkedin-country-timezones.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Primary IANA time zone for each LinkedIn country code.
|
|
3
|
+
*
|
|
4
|
+
* WHY: a LinkedIn sender's working hours and daily-quota rollover run in the
|
|
5
|
+
* account owner's local time. The only geo signal OXYGEN holds for a new
|
|
6
|
+
* account is the country the owner chose at connect ("where the account owner
|
|
7
|
+
* normally uses LinkedIn"), so that country picks the zone whenever no explicit
|
|
8
|
+
* timezone was stored. Without it every account ran on America/New_York and a
|
|
9
|
+
* German account was active 13:00–04:00 Berlin time.
|
|
10
|
+
*
|
|
11
|
+
* GENERATED once from ICU's region → zone data (`new Intl.Locale("und-XX")
|
|
12
|
+
* .timeZones`) for the codes in LINKEDIN_COUNTRIES, then frozen here so a Node
|
|
13
|
+
* upgrade cannot silently move an account's window. Countries spanning several
|
|
14
|
+
* zones carry the zone of their largest business population (US → New York,
|
|
15
|
+
* CA → Toronto, AU → Sydney, BR → São Paulo, RU → Moscow, …). Territories with
|
|
16
|
+
* no populated zone (AQ, BV, HM) are absent and fall back to the default. An
|
|
17
|
+
* explicit per-account timezone always wins over this table.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LINKEDIN_COUNTRY_PRIMARY_TIME_ZONES: Readonly<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* The primary IANA zone for an ISO 3166-1 alpha-2 country code (any case), or
|
|
22
|
+
* null when the code is unknown or has no populated zone.
|
|
23
|
+
*/
|
|
24
|
+
export declare function linkedInCountryPrimaryTimeZone(country: unknown): string | null;
|