@apifuse/provider-sdk 2.2.0-beta.52 → 2.2.0-beta.54
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/CHANGELOG.md +8 -0
- package/dist/cli/migrate-operation-declaration.d.ts +1 -1
- package/dist/cli/migrate-operation-declaration.js +126 -5
- package/dist/i18n/operation-locale-namespace.d.ts +2 -0
- package/dist/i18n/operation-locale-namespace.js +7 -0
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/trace.d.ts +7 -0
- package/dist/runtime/trace.js +24 -1
- package/dist/server/serve-implementation.d.ts +4 -0
- package/dist/server/serve-implementation.js +576 -321
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +17 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +8 -0
- package/src/cli/migrate-operation-declaration.ts +208 -6
- package/src/i18n/operation-locale-namespace.ts +10 -0
- package/src/runtime/auth-flow.ts +3 -0
- package/src/runtime/trace.ts +37 -1
- package/src/server/serve-implementation.ts +759 -586
- package/src/types.ts +1 -0
package/dist/types.d.ts
CHANGED
|
@@ -1748,6 +1748,7 @@ export interface FlowContext {
|
|
|
1748
1748
|
externalRef?: string;
|
|
1749
1749
|
tenantId: string;
|
|
1750
1750
|
providerId: string;
|
|
1751
|
+
trace: TraceContext;
|
|
1751
1752
|
http: HttpClient;
|
|
1752
1753
|
/** Durable connection-scoped runtime state. Present when the host runtime
|
|
1753
1754
|
* supplies one; auth ceremonies must fail closed when absent rather than
|
package/package.json
CHANGED
package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const listHospitalsOperation = defineOperation<ProviderContext>()({
|
|
2
|
+
annotations: { readOnly: true },
|
|
3
|
+
inputExamples: [
|
|
4
|
+
{
|
|
5
|
+
scenario: "List nearby hospitals",
|
|
6
|
+
input: { latitude: 37.5665, longitude: 126.978 },
|
|
7
|
+
rationale: "Exercises a kebab-case operation id.",
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
input: InputSchema,
|
|
11
|
+
output: OutputSchema,
|
|
12
|
+
handler,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default buildProvider({
|
|
16
|
+
operations: { "list-hospitals": listHospitalsOperation },
|
|
17
|
+
});
|
package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const listRecentEarthquakesOperation = defineOperation<ProviderContext>()({
|
|
2
|
+
annotations: { readOnly: true },
|
|
3
|
+
docs: {
|
|
4
|
+
titleKey: "operations.listRecentEarthquakes.title",
|
|
5
|
+
descriptionKey: "operations.listRecentEarthquakes.description",
|
|
6
|
+
},
|
|
7
|
+
inputExamples: [
|
|
8
|
+
{
|
|
9
|
+
scenario: "List recent earthquakes",
|
|
10
|
+
input: { limit: 10 },
|
|
11
|
+
rationale: "Exercises a snake-case operation id.",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
input: InputSchema,
|
|
15
|
+
output: OutputSchema,
|
|
16
|
+
handler,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default buildProvider({
|
|
20
|
+
operations: { list_recent_earthquakes: listRecentEarthquakesOperation },
|
|
21
|
+
});
|
|
@@ -3,6 +3,9 @@ import { dirname, extname, join, relative, resolve } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import type TS from "typescript";
|
|
5
5
|
|
|
6
|
+
import { assertProviderLocaleKey } from "../i18n/keys.js";
|
|
7
|
+
import { operationIdToLocaleNamespace } from "../i18n/operation-locale-namespace.js";
|
|
8
|
+
|
|
6
9
|
const ts: typeof import("typescript") = await loadTypeScript();
|
|
7
10
|
|
|
8
11
|
async function loadTypeScript(): Promise<typeof import("typescript")> {
|
|
@@ -67,6 +70,7 @@ export type OperationDeclarationRefusalReason =
|
|
|
67
70
|
| "missing_english_locale"
|
|
68
71
|
| "operation_id_unresolved"
|
|
69
72
|
| "examples_conflict"
|
|
73
|
+
| "invalid_locale_key"
|
|
70
74
|
| "locale_todo_conflict";
|
|
71
75
|
|
|
72
76
|
export type OperationDeclarationRefusal = {
|
|
@@ -180,6 +184,10 @@ export function migrateOperationDeclaration(
|
|
|
180
184
|
todos.push(...plan.localeTodos);
|
|
181
185
|
}
|
|
182
186
|
if (refusals.length > 0) return { status: "refused", refusals };
|
|
187
|
+
const invalidLocaleKey = findInvalidLocaleTodo(todos, fileName);
|
|
188
|
+
if (invalidLocaleKey !== undefined) {
|
|
189
|
+
return { status: "refused", refusals: [invalidLocaleKey] };
|
|
190
|
+
}
|
|
183
191
|
|
|
184
192
|
if (edits.length === 0) {
|
|
185
193
|
return {
|
|
@@ -400,6 +408,30 @@ function planOperationMigration(
|
|
|
400
408
|
if (merged.insert !== undefined) addInsertion(insertions, "docs", merged.insert);
|
|
401
409
|
}
|
|
402
410
|
|
|
411
|
+
const title = planTitleLocale(
|
|
412
|
+
top.byName.get("title"),
|
|
413
|
+
top.byName.get("titleKey"),
|
|
414
|
+
docs.get("titleKey"),
|
|
415
|
+
fileName,
|
|
416
|
+
site,
|
|
417
|
+
localeFiles,
|
|
418
|
+
);
|
|
419
|
+
if ("refusal" in title) return title;
|
|
420
|
+
const localeNamespace =
|
|
421
|
+
top.byName.get("inputExamples") === undefined
|
|
422
|
+
? { namespace: site.operationKey }
|
|
423
|
+
: resolveOperationLocaleNamespace(
|
|
424
|
+
[
|
|
425
|
+
top.byName.get("titleKey"),
|
|
426
|
+
docs.get("titleKey"),
|
|
427
|
+
top.byName.get("descriptionKey"),
|
|
428
|
+
docs.get("descriptionKey"),
|
|
429
|
+
],
|
|
430
|
+
fileName,
|
|
431
|
+
site,
|
|
432
|
+
);
|
|
433
|
+
if ("refusal" in localeNamespace) return localeNamespace;
|
|
434
|
+
|
|
403
435
|
const examples = planExamples(
|
|
404
436
|
top.byName.get("inputExamples"),
|
|
405
437
|
top.byName.get("examples"),
|
|
@@ -408,6 +440,7 @@ function planOperationMigration(
|
|
|
408
440
|
source,
|
|
409
441
|
constArrays,
|
|
410
442
|
localeFiles,
|
|
443
|
+
localeNamespace.namespace,
|
|
411
444
|
);
|
|
412
445
|
if ("refusal" in examples) return examples;
|
|
413
446
|
|
|
@@ -431,7 +464,107 @@ function planOperationMigration(
|
|
|
431
464
|
if (!edits.some((existing) => rangesOverlap(existing, edit))) edits.push(edit);
|
|
432
465
|
}
|
|
433
466
|
|
|
434
|
-
return { edits, localeTodos: examples.localeTodos };
|
|
467
|
+
return { edits, localeTodos: [...title.localeTodos, ...examples.localeTodos] };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function planTitleLocale(
|
|
471
|
+
title: ResolvedMember | undefined,
|
|
472
|
+
flatTitleKey: ResolvedMember | undefined,
|
|
473
|
+
nestedTitleKey: ResolvedMember | undefined,
|
|
474
|
+
fileName: string,
|
|
475
|
+
site: OperationSite,
|
|
476
|
+
localeFiles: readonly string[],
|
|
477
|
+
):
|
|
478
|
+
| { readonly localeTodos: readonly LocaleTodo[] }
|
|
479
|
+
| { readonly refusal: OperationDeclarationRefusal } {
|
|
480
|
+
if (title === undefined) return { localeTodos: [] };
|
|
481
|
+
const originalProse = literalString(title.initializer);
|
|
482
|
+
if (originalProse === undefined) {
|
|
483
|
+
return nonLiteral(
|
|
484
|
+
fileName,
|
|
485
|
+
site.operationKey,
|
|
486
|
+
"title must be a string literal so its authored prose can be preserved in the English locale catalog.",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (!site.operationIdProven) {
|
|
490
|
+
return {
|
|
491
|
+
refusal: refusal(
|
|
492
|
+
fileName,
|
|
493
|
+
site.operationKey,
|
|
494
|
+
"operation_id_unresolved",
|
|
495
|
+
"title requires an exact operation id proven from a static operations map.",
|
|
496
|
+
),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (!localeFiles.includes("locales/en.json")) {
|
|
500
|
+
return {
|
|
501
|
+
refusal: refusal(
|
|
502
|
+
fileName,
|
|
503
|
+
site.operationKey,
|
|
504
|
+
"missing_english_locale",
|
|
505
|
+
"title cannot be migrated because locales/en.json does not exist.",
|
|
506
|
+
),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let selectedTitleKey = flatTitleKey ?? nestedTitleKey;
|
|
511
|
+
if (flatTitleKey !== undefined && nestedTitleKey !== undefined) {
|
|
512
|
+
const same = equivalentLiteral(flatTitleKey.initializer, nestedTitleKey.initializer);
|
|
513
|
+
if (same === undefined) {
|
|
514
|
+
return nonLiteral(
|
|
515
|
+
fileName,
|
|
516
|
+
site.operationKey,
|
|
517
|
+
"titleKey must be literal when both top-level and nested declarations exist.",
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (!same) {
|
|
521
|
+
return {
|
|
522
|
+
refusal: refusal(
|
|
523
|
+
fileName,
|
|
524
|
+
site.operationKey,
|
|
525
|
+
"locale_key_conflict",
|
|
526
|
+
"Top-level titleKey conflicts with nested titleKey.",
|
|
527
|
+
),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
selectedTitleKey = flatTitleKey;
|
|
531
|
+
}
|
|
532
|
+
const explicitTitleKey =
|
|
533
|
+
selectedTitleKey === undefined ? undefined : literalString(selectedTitleKey.initializer);
|
|
534
|
+
if (selectedTitleKey !== undefined && explicitTitleKey === undefined) {
|
|
535
|
+
return nonLiteral(
|
|
536
|
+
fileName,
|
|
537
|
+
site.operationKey,
|
|
538
|
+
"titleKey must be a string literal so the title locale destination is provable.",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
let selectedTitleLocaleKey: string;
|
|
542
|
+
if (explicitTitleKey !== undefined) {
|
|
543
|
+
selectedTitleLocaleKey = explicitTitleKey;
|
|
544
|
+
} else {
|
|
545
|
+
try {
|
|
546
|
+
selectedTitleLocaleKey = `operations.${operationIdToLocaleNamespace(site.operationKey)}.title`;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return {
|
|
549
|
+
refusal: invalidLocaleKeyRefusal(
|
|
550
|
+
fileName,
|
|
551
|
+
site.operationKey,
|
|
552
|
+
`operations.${site.operationKey}.title`,
|
|
553
|
+
error,
|
|
554
|
+
),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
localeTodos: [
|
|
560
|
+
{
|
|
561
|
+
localeFile: "locales/en.json",
|
|
562
|
+
operationKey: site.operationKey,
|
|
563
|
+
key: selectedTitleLocaleKey,
|
|
564
|
+
originalProse,
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
};
|
|
435
568
|
}
|
|
436
569
|
|
|
437
570
|
function resolveRiskClass(
|
|
@@ -664,6 +797,7 @@ function planExamples(
|
|
|
664
797
|
source: TS.SourceFile,
|
|
665
798
|
constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
|
|
666
799
|
localeFiles: readonly string[],
|
|
800
|
+
localeNamespace: string,
|
|
667
801
|
):
|
|
668
802
|
| { readonly edits: readonly TextEdit[]; readonly localeTodos: readonly LocaleTodo[] }
|
|
669
803
|
| { readonly refusal: OperationDeclarationRefusal } {
|
|
@@ -760,7 +894,7 @@ function planExamples(
|
|
|
760
894
|
`inputExamples[${index}].scenario must be a string literal.`,
|
|
761
895
|
);
|
|
762
896
|
}
|
|
763
|
-
const scenarioKey = `operations.${
|
|
897
|
+
const scenarioKey = `operations.${localeNamespace}.examples.${index}.scenario`;
|
|
764
898
|
edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
|
|
765
899
|
for (const localeFile of localeFiles) {
|
|
766
900
|
todos.push({
|
|
@@ -781,7 +915,7 @@ function planExamples(
|
|
|
781
915
|
`inputExamples[${index}].rationale must be a string literal.`,
|
|
782
916
|
);
|
|
783
917
|
}
|
|
784
|
-
const rationaleKey = `operations.${
|
|
918
|
+
const rationaleKey = `operations.${localeNamespace}.examples.${index}.rationale`;
|
|
785
919
|
edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
|
|
786
920
|
for (const localeFile of localeFiles) {
|
|
787
921
|
todos.push({
|
|
@@ -796,6 +930,47 @@ function planExamples(
|
|
|
796
930
|
return { edits, localeTodos: todos };
|
|
797
931
|
}
|
|
798
932
|
|
|
933
|
+
function resolveOperationLocaleNamespace(
|
|
934
|
+
members: readonly (ResolvedMember | undefined)[],
|
|
935
|
+
fileName: string,
|
|
936
|
+
site: OperationSite,
|
|
937
|
+
): { readonly namespace: string } | { readonly refusal: OperationDeclarationRefusal } {
|
|
938
|
+
const authoredNamespaces = new Set<string>();
|
|
939
|
+
for (const member of members) {
|
|
940
|
+
const localeKey = literalString(member?.initializer);
|
|
941
|
+
if (localeKey === undefined) continue;
|
|
942
|
+
const segments = localeKey.split(".");
|
|
943
|
+
if (segments[0] === "operations" && segments[1] !== undefined) {
|
|
944
|
+
authoredNamespaces.add(segments[1]);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (authoredNamespaces.size > 1) {
|
|
948
|
+
return {
|
|
949
|
+
refusal: refusal(
|
|
950
|
+
fileName,
|
|
951
|
+
site.operationKey,
|
|
952
|
+
"locale_key_conflict",
|
|
953
|
+
`Operation titleKey and descriptionKey declarations use different locale namespaces: ${[...authoredNamespaces].join(", ")}.`,
|
|
954
|
+
),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const authored = authoredNamespaces.values().next().value;
|
|
958
|
+
if (authored !== undefined) return { namespace: authored };
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
return { namespace: operationIdToLocaleNamespace(site.operationKey) };
|
|
962
|
+
} catch (error) {
|
|
963
|
+
return {
|
|
964
|
+
refusal: invalidLocaleKeyRefusal(
|
|
965
|
+
fileName,
|
|
966
|
+
site.operationKey,
|
|
967
|
+
`operations.${site.operationKey}.examples`,
|
|
968
|
+
error,
|
|
969
|
+
),
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
799
974
|
function replaceExampleLocaleMember(
|
|
800
975
|
member: ResolvedMember,
|
|
801
976
|
newName: string,
|
|
@@ -984,9 +1159,7 @@ function isProviderOperationsProperty(
|
|
|
984
1159
|
// (a) The initializer (direct or via same-file const) mentions
|
|
985
1160
|
// defineOperation / defineStreamOperation — the strongest signal.
|
|
986
1161
|
const target = ts.isIdentifier(unwrapExpression(node.initializer) ?? node.initializer)
|
|
987
|
-
? constObjects.get(
|
|
988
|
-
(unwrapExpression(node.initializer) as TS.Identifier).text,
|
|
989
|
-
)
|
|
1162
|
+
? constObjects.get((unwrapExpression(node.initializer) as TS.Identifier).text)
|
|
990
1163
|
: undefined;
|
|
991
1164
|
const initializerText = (target ?? node.initializer).getText();
|
|
992
1165
|
if (/\bdefine(?:Stream)?Operation\b/.test(initializerText)) return true;
|
|
@@ -1513,6 +1686,35 @@ function refusal(
|
|
|
1513
1686
|
return { file, operationKey, reason, detail };
|
|
1514
1687
|
}
|
|
1515
1688
|
|
|
1689
|
+
function findInvalidLocaleTodo(
|
|
1690
|
+
todos: readonly LocaleTodo[],
|
|
1691
|
+
fileName: string,
|
|
1692
|
+
): OperationDeclarationRefusal | undefined {
|
|
1693
|
+
for (const todo of todos) {
|
|
1694
|
+
try {
|
|
1695
|
+
assertProviderLocaleKey(todo.key);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
return invalidLocaleKeyRefusal(fileName, todo.operationKey, todo.key, error);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return undefined;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function invalidLocaleKeyRefusal(
|
|
1704
|
+
fileName: string,
|
|
1705
|
+
operationKey: string,
|
|
1706
|
+
localeKey: string,
|
|
1707
|
+
error: unknown,
|
|
1708
|
+
): OperationDeclarationRefusal {
|
|
1709
|
+
const validatorDetail = error instanceof Error ? error.message : String(error);
|
|
1710
|
+
return refusal(
|
|
1711
|
+
fileName,
|
|
1712
|
+
operationKey,
|
|
1713
|
+
"invalid_locale_key",
|
|
1714
|
+
`Refusing to write invalid provider locale key ${JSON.stringify(localeKey)}: ${validatorDetail}`,
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1516
1718
|
function nonLiteral(
|
|
1517
1719
|
fileName: string,
|
|
1518
1720
|
operationKey: string,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { assertProviderLocaleKey } from "./keys.js";
|
|
2
|
+
|
|
3
|
+
/** Canonical locale-catalog namespace for a URL-safe provider operation id. */
|
|
4
|
+
export function operationIdToLocaleNamespace(operationId: string): string {
|
|
5
|
+
const namespace = operationId.replace(/[-_]([a-z0-9])/g, (_separator, character: string) =>
|
|
6
|
+
character.toUpperCase(),
|
|
7
|
+
);
|
|
8
|
+
assertProviderLocaleKey(`operations.${namespace}.description`);
|
|
9
|
+
return namespace;
|
|
10
|
+
}
|
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import { createUnsupportedOcrClient } from "./ocr.js";
|
|
14
14
|
import { createUnsupportedResolverClient } from "./resolver-shared.js";
|
|
15
15
|
import { createUnsupportedSttClient } from "./stt.js";
|
|
16
|
+
import { createTraceContext } from "./trace.js";
|
|
16
17
|
|
|
17
18
|
function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
|
|
18
19
|
return new Set(allowedKeys.filter((key) => key.trim().length > 0));
|
|
@@ -66,6 +67,7 @@ export function createFlowContext(options: {
|
|
|
66
67
|
initialContext?: Record<string, unknown>;
|
|
67
68
|
ocr?: OcrContext;
|
|
68
69
|
stt?: SttContext;
|
|
70
|
+
trace?: FlowContext["trace"];
|
|
69
71
|
}): FlowContext {
|
|
70
72
|
return {
|
|
71
73
|
flowId: options.flowId,
|
|
@@ -73,6 +75,7 @@ export function createFlowContext(options: {
|
|
|
73
75
|
externalRef: options.externalRef,
|
|
74
76
|
tenantId: options.tenantId,
|
|
75
77
|
providerId: options.providerId,
|
|
78
|
+
trace: options.trace ?? createTraceContext(),
|
|
76
79
|
http: options.http,
|
|
77
80
|
state: options.state,
|
|
78
81
|
stealth: options.stealth,
|
package/src/runtime/trace.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface CreateTraceContextOptions {
|
|
|
21
21
|
onSpan?: (span: Span) => void;
|
|
22
22
|
exportOptions?: OTLPExportOptions;
|
|
23
23
|
resourceAttributes?: Record<string, string>;
|
|
24
|
+
/** W3C-compatible 32-character lowercase hexadecimal trace id used for export. */
|
|
25
|
+
traceId?: string;
|
|
24
26
|
/**
|
|
25
27
|
* Applied to a detached copy of each span immediately before OTLP export; never touches
|
|
26
28
|
* getSpans() or onSpan. Returning nothing (or throwing) drops that export batch.
|
|
@@ -55,11 +57,33 @@ export interface TraceRecorder {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-recorder");
|
|
60
|
+
const TRACE_EXPORT_METADATA = Symbol.for(
|
|
61
|
+
"@apifuse/provider-sdk/runtime/trace-export-metadata",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
type TraceExportMetadata = {
|
|
65
|
+
update(input: { traceId?: string; resourceAttributes?: Record<string, string> }): void;
|
|
66
|
+
};
|
|
58
67
|
|
|
59
68
|
type InternalTraceContext = TraceContext & {
|
|
60
69
|
[TRACE_RECORDER]: TraceRecorder;
|
|
70
|
+
[TRACE_EXPORT_METADATA]: TraceExportMetadata;
|
|
61
71
|
};
|
|
62
72
|
|
|
73
|
+
function assertValidTraceId(traceId: string): void {
|
|
74
|
+
if (!/^[0-9a-f]{32}$/.test(traceId) || /^0{32}$/.test(traceId)) {
|
|
75
|
+
throw new TypeError("traceId must be 32 lowercase hexadecimal characters and non-zero");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Updates request metadata before its pending root span completes and exports. */
|
|
80
|
+
export function updateTraceContextExportMetadata(
|
|
81
|
+
trace: BaseTraceContext,
|
|
82
|
+
input: { traceId?: string; resourceAttributes?: Record<string, string> },
|
|
83
|
+
): void {
|
|
84
|
+
(trace as Partial<InternalTraceContext>)[TRACE_EXPORT_METADATA]?.update(input);
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
function buildOTLPExportOptions(config?: TraceConfig): OTLPExportOptions | undefined {
|
|
64
88
|
if (config?.exporter !== "otlp") {
|
|
65
89
|
return undefined;
|
|
@@ -148,6 +172,7 @@ export function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null
|
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
export function createTraceContext(options: CreateTraceContextOptions = {}): TraceContext {
|
|
175
|
+
if (options.traceId !== undefined) assertValidTraceId(options.traceId);
|
|
151
176
|
const maxSpans = options.maxSpans ?? 1000;
|
|
152
177
|
const completed: CompletedSpanEntry[] = [];
|
|
153
178
|
const activeSpanStorage = new AsyncLocalStorage<PendingSpan | undefined>();
|
|
@@ -160,7 +185,7 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
160
185
|
: undefined;
|
|
161
186
|
// One trace id per context so every export batch of this request shares it and
|
|
162
187
|
// two processes can never mint the same id.
|
|
163
|
-
|
|
188
|
+
let exportTraceId = options.traceId ?? crypto.randomUUID().replace(/-/g, "");
|
|
164
189
|
let exportScheduled = false;
|
|
165
190
|
|
|
166
191
|
// One pending batch per context: roots completing before the flush share it, and a span is
|
|
@@ -265,6 +290,17 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
|
|
|
265
290
|
return completed.map((entry) => ({ ...entry.span }));
|
|
266
291
|
},
|
|
267
292
|
[TRACE_RECORDER]: recorder,
|
|
293
|
+
[TRACE_EXPORT_METADATA]: {
|
|
294
|
+
update(input) {
|
|
295
|
+
if (input.traceId !== undefined) {
|
|
296
|
+
assertValidTraceId(input.traceId);
|
|
297
|
+
exportTraceId = input.traceId;
|
|
298
|
+
}
|
|
299
|
+
if (input.resourceAttributes !== undefined && exportResourceAttributes) {
|
|
300
|
+
Object.assign(exportResourceAttributes, input.resourceAttributes);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
},
|
|
268
304
|
};
|
|
269
305
|
|
|
270
306
|
return traceContext;
|