@graphorin/observability 0.5.0 → 0.6.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +10 -6
  3. package/dist/cost/cost-tracker.js.map +1 -1
  4. package/dist/cost/types.d.ts +1 -1
  5. package/dist/eval/runner.js.map +1 -1
  6. package/dist/eval/types.d.ts +19 -1
  7. package/dist/eval/types.d.ts.map +1 -1
  8. package/dist/exporters/console.js.map +1 -1
  9. package/dist/exporters/jsonl.d.ts +1 -1
  10. package/dist/exporters/jsonl.js +2 -2
  11. package/dist/exporters/jsonl.js.map +1 -1
  12. package/dist/exporters/otlp-http.d.ts +2 -2
  13. package/dist/exporters/otlp-http.d.ts.map +1 -1
  14. package/dist/exporters/otlp-http.js +6 -4
  15. package/dist/exporters/otlp-http.js.map +1 -1
  16. package/dist/exporters/types.d.ts +2 -2
  17. package/dist/exporters/types.js +1 -1
  18. package/dist/exporters/types.js.map +1 -1
  19. package/dist/exporters/with-validation.d.ts +1 -1
  20. package/dist/exporters/with-validation.js.map +1 -1
  21. package/dist/gen-ai/emit.d.ts +1 -1
  22. package/dist/gen-ai/emit.js +1 -1
  23. package/dist/gen-ai/emit.js.map +1 -1
  24. package/dist/gen-ai/operation-mapping.d.ts +1 -1
  25. package/dist/gen-ai/operation-mapping.d.ts.map +1 -1
  26. package/dist/gen-ai/operation-mapping.js +3 -3
  27. package/dist/gen-ai/operation-mapping.js.map +1 -1
  28. package/dist/gen-ai/types.d.ts +2 -2
  29. package/dist/gen-ai/types.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/logger/logger.js +1 -1
  35. package/dist/logger/logger.js.map +1 -1
  36. package/dist/openinference/index.d.ts +2 -2
  37. package/dist/openinference/index.js +2 -2
  38. package/dist/openinference/index.js.map +1 -1
  39. package/dist/package.js +6 -0
  40. package/dist/package.js.map +1 -0
  41. package/dist/redaction/errors.js +1 -1
  42. package/dist/redaction/errors.js.map +1 -1
  43. package/dist/redaction/imperative-patterns.d.ts +3 -3
  44. package/dist/redaction/imperative-patterns.js +2 -2
  45. package/dist/redaction/imperative-patterns.js.map +1 -1
  46. package/dist/redaction/patterns.d.ts +6 -6
  47. package/dist/redaction/patterns.d.ts.map +1 -1
  48. package/dist/redaction/patterns.js +5 -5
  49. package/dist/redaction/patterns.js.map +1 -1
  50. package/dist/redaction/types.d.ts +1 -1
  51. package/dist/redaction/validator.js +2 -2
  52. package/dist/redaction/validator.js.map +1 -1
  53. package/dist/replay/config.d.ts +1 -1
  54. package/dist/replay/config.js.map +1 -1
  55. package/dist/replay/log.js +1 -1
  56. package/dist/replay/log.js.map +1 -1
  57. package/dist/replay/replay.js.map +1 -1
  58. package/dist/replay/types.d.ts +2 -2
  59. package/dist/telemetry/index.js +2 -2
  60. package/dist/telemetry/index.js.map +1 -1
  61. package/dist/tracer/sampling.d.ts +2 -2
  62. package/dist/tracer/sampling.js +1 -1
  63. package/dist/tracer/sampling.js.map +1 -1
  64. package/dist/tracer/span-names.d.ts +1 -1
  65. package/dist/tracer/span-names.d.ts.map +1 -1
  66. package/dist/tracer/span-names.js +25 -2
  67. package/dist/tracer/span-names.js.map +1 -1
  68. package/dist/tracer/span.js.map +1 -1
  69. package/dist/tracer/tracer.d.ts +1 -1
  70. package/dist/tracer/tracer.js +2 -2
  71. package/dist/tracer/tracer.js.map +1 -1
  72. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"validator.js","names":["out: RedactionPattern[]","out","out: Record<string, unknown>","minTier: Sensitivity","out: RedactionOutput"],"sources":["../../src/redaction/validator.ts"],"sourcesContent":["/**\n * `createRedactionValidator(...)` — the building block for the\n * mandatory `withValidation()` wrapper applied to every exporter.\n *\n * @packageDocumentation\n */\n\nimport { SENSITIVITY_ORDER, type Sensitivity } from '@graphorin/core';\n\nimport { RedactionValidationError } from './errors.js';\nimport { BUILT_IN_PATTERNS, type RedactionPattern } from './patterns.js';\nimport type {\n RedactionCounters,\n RedactionInput,\n RedactionOutput,\n RedactionValidator,\n RedactionValidatorInstance,\n RedactionValidatorOptions,\n RedactionViolation,\n} from './types.js';\n\nconst TIER_INDEX = new Map<Sensitivity, number>(\n SENSITIVITY_ORDER.map((tier, idx) => [tier, idx] as const),\n);\n\nfunction rank(tier: Sensitivity): number {\n const idx = TIER_INDEX.get(tier);\n return idx ?? 0;\n}\n\ninterface CountersInternal {\n droppedTotal: number;\n droppedByReason: Map<string, number>;\n matchesByPattern: Map<string, number>;\n}\n\nfunction makeCounters(): CountersInternal {\n return {\n droppedTotal: 0,\n droppedByReason: new Map(),\n matchesByPattern: new Map(),\n };\n}\n\nfunction bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1);\n}\n\nfunction freezeCounters(c: CountersInternal): RedactionCounters {\n return {\n droppedTotal: c.droppedTotal,\n droppedByReason: Object.freeze(Object.fromEntries(c.droppedByReason)),\n matchesByPattern: Object.freeze(Object.fromEntries(c.matchesByPattern)),\n };\n}\n\nfunction selectPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n enabled?: ReadonlyArray<string>,\n disabled?: ReadonlyArray<string>,\n): RedactionPattern[] {\n const enabledSet = enabled === undefined ? null : new Set(enabled);\n const disabledSet = new Set(disabled ?? []);\n const out: RedactionPattern[] = [];\n for (const p of patterns) {\n if (enabledSet !== null && !enabledSet.has(p.name)) continue;\n if (disabledSet.has(p.name)) continue;\n out.push(p);\n }\n return out;\n}\n\n/**\n * Walk every string discovered in `value` and apply `fn`. Returns\n * `{ output, matched }` where `output` is the rewritten payload and\n * `matched` is the de-duplicated list of pattern names that fired.\n *\n * The walker handles strings, plain objects, and arrays. Other JS\n * primitives are passed through untouched. Cycles are detected via a\n * `WeakSet` and broken with `'[Circular]'`.\n */\nfunction walk(\n value: unknown,\n fn: (s: string, matched: Set<string>) => string,\n matched: Set<string>,\n seen: WeakSet<object>,\n): unknown {\n if (typeof value === 'string') return fn(value, matched);\n if (value === null || typeof value !== 'object') return value;\n if (seen.has(value as object)) return '[Circular]';\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n const out = new Array<unknown>(value.length);\n for (let i = 0; i < value.length; i++) {\n out[i] = walk(value[i], fn, matched, seen);\n }\n return out;\n }\n\n const obj = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n out[key] = walk(obj[key], fn, matched, seen);\n }\n return out;\n}\n\n/**\n * Apply every active pattern to `s`. Mutates `matched` with the names\n * that fired and returns the rewritten string.\n */\nfunction applyPatterns(\n s: string,\n patterns: ReadonlyArray<RedactionPattern>,\n matched: Set<string>,\n): string {\n let out = s;\n for (const p of patterns) {\n const mask = p.mask ?? `[REDACTED ${p.name}]`;\n p.regex.lastIndex = 0;\n if (p.verify === undefined) {\n if (p.regex.test(out)) {\n matched.add(p.name);\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, mask);\n }\n continue;\n }\n // RP-21: per-match predicate — only mask hits the verifier accepts (e.g.\n // Luhn-valid PANs), and only count the pattern as matched when one did.\n const verify = p.verify;\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, (m) => {\n if (verify(m)) {\n matched.add(p.name);\n return mask;\n }\n return m;\n });\n }\n return out;\n}\n\nfunction violationFor(\n reason: RedactionViolation['reason'],\n input: RedactionInput,\n patterns?: ReadonlyArray<string>,\n): RedactionViolation {\n const baseContext = input.context;\n const base: RedactionViolation = {\n reason,\n declaredTier: input.tier,\n ...(baseContext?.attribute === undefined ? {} : { attribute: baseContext.attribute }),\n ...(baseContext?.spanType === undefined ? {} : { spanType: baseContext.spanType }),\n ...(baseContext?.origin === undefined ? {} : { origin: baseContext.origin }),\n ...(patterns === undefined || patterns.length === 0 ? {} : { patterns }),\n };\n return base;\n}\n\n/**\n * Create a {@link RedactionValidator} configured against the supplied\n * options. The result implements both the `RedactionValidator`\n * contract from `@graphorin/core` and the\n * {@link RedactionValidatorInstance} extension surface (counters +\n * reset).\n *\n * @stable\n */\nexport function createRedactionValidator(\n opts: RedactionValidatorOptions = {},\n): RedactionValidatorInstance {\n const id = opts.id ?? 'default';\n const minTier: Sensitivity = opts.minTier ?? 'public';\n const failOnUnredacted = opts.failOnUnredactedSensitive === true;\n const patterns = selectPatterns(\n opts.patterns ?? BUILT_IN_PATTERNS,\n opts.enabledPatterns,\n opts.disabledPatterns,\n );\n const onViolation = opts.onViolation;\n const custom = opts.customValidator;\n\n let counters = makeCounters();\n\n function recordViolation(violation: RedactionViolation): void {\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n for (const name of violation.patterns ?? []) {\n bump(counters.matchesByPattern, name);\n }\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n // Listener errors must never break the exporter.\n }\n }\n }\n\n function dropOrThrow(_input: RedactionInput, violation: RedactionViolation): null {\n recordViolation(violation);\n if (failOnUnredacted) {\n throw new RedactionValidationError(\n `RedactionValidator dropped value (${violation.reason}) — set ` +\n 'failOnUnredactedSensitive: false in production to keep the ' +\n 'pipeline running.',\n violation,\n );\n }\n return null;\n }\n\n const validator: RedactionValidator = {\n id,\n minTier,\n validate(input: RedactionInput): RedactionOutput | null {\n if (input === null || input === undefined || typeof input !== 'object') {\n return dropOrThrow(\n input as RedactionInput,\n violationFor('invalid-input', input as RedactionInput),\n );\n }\n\n const declared = input.tier ?? 'internal';\n\n if (rank(declared) > rank(minTier)) {\n return dropOrThrow(input, violationFor('sensitivity-tier-exceeded', input));\n }\n\n const matched = new Set<string>();\n const rewritten = walk(\n input.value,\n (s, m) => applyPatterns(s, patterns, m),\n matched,\n new WeakSet(),\n );\n\n if (matched.size > 0) {\n const matchedList = [...matched];\n for (const name of matchedList) {\n bump(counters.matchesByPattern, name);\n }\n const containsSecret = matchedList.some(\n (name) => patterns.find((p) => p.name === name)?.category === 'secret',\n );\n\n if (containsSecret) {\n if (failOnUnredacted) {\n const violation = violationFor('secret-pattern-match', input, matchedList);\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n /* listener errors swallowed */\n }\n }\n throw new RedactionValidationError(\n `RedactionValidator detected secret pattern(s) in attribute (${matchedList.join(', ')}).`,\n violation,\n );\n }\n }\n }\n\n const out: RedactionOutput =\n matched.size === 0\n ? { value: rewritten, tier: declared }\n : { value: rewritten, tier: declared, matched: [...matched] };\n\n if (custom !== undefined) {\n return custom({ ...input, value: rewritten });\n }\n\n return out;\n },\n };\n\n const instance: RedactionValidatorInstance = {\n ...validator,\n counters: () => freezeCounters(counters),\n resetCounters: () => {\n counters = makeCounters();\n },\n };\n\n return instance;\n}\n\n/**\n * Quickly compute the relative ordering of two sensitivity tiers.\n * Exposed because the tracer + replay layers need it without taking a\n * full dependency on the validator implementation.\n *\n * @stable\n */\nexport function compareSensitivityTiers(a: Sensitivity, b: Sensitivity): number {\n return rank(a) - rank(b);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validator.js","names":["out: RedactionPattern[]","out","out: Record<string, unknown>","minTier: Sensitivity","out: RedactionOutput"],"sources":["../../src/redaction/validator.ts"],"sourcesContent":["/**\n * `createRedactionValidator(...)` - the building block for the\n * mandatory `withValidation()` wrapper applied to every exporter.\n *\n * @packageDocumentation\n */\n\nimport { SENSITIVITY_ORDER, type Sensitivity } from '@graphorin/core';\n\nimport { RedactionValidationError } from './errors.js';\nimport { BUILT_IN_PATTERNS, type RedactionPattern } from './patterns.js';\nimport type {\n RedactionCounters,\n RedactionInput,\n RedactionOutput,\n RedactionValidator,\n RedactionValidatorInstance,\n RedactionValidatorOptions,\n RedactionViolation,\n} from './types.js';\n\nconst TIER_INDEX = new Map<Sensitivity, number>(\n SENSITIVITY_ORDER.map((tier, idx) => [tier, idx] as const),\n);\n\nfunction rank(tier: Sensitivity): number {\n const idx = TIER_INDEX.get(tier);\n return idx ?? 0;\n}\n\ninterface CountersInternal {\n droppedTotal: number;\n droppedByReason: Map<string, number>;\n matchesByPattern: Map<string, number>;\n}\n\nfunction makeCounters(): CountersInternal {\n return {\n droppedTotal: 0,\n droppedByReason: new Map(),\n matchesByPattern: new Map(),\n };\n}\n\nfunction bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1);\n}\n\nfunction freezeCounters(c: CountersInternal): RedactionCounters {\n return {\n droppedTotal: c.droppedTotal,\n droppedByReason: Object.freeze(Object.fromEntries(c.droppedByReason)),\n matchesByPattern: Object.freeze(Object.fromEntries(c.matchesByPattern)),\n };\n}\n\nfunction selectPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n enabled?: ReadonlyArray<string>,\n disabled?: ReadonlyArray<string>,\n): RedactionPattern[] {\n const enabledSet = enabled === undefined ? null : new Set(enabled);\n const disabledSet = new Set(disabled ?? []);\n const out: RedactionPattern[] = [];\n for (const p of patterns) {\n if (enabledSet !== null && !enabledSet.has(p.name)) continue;\n if (disabledSet.has(p.name)) continue;\n out.push(p);\n }\n return out;\n}\n\n/**\n * Walk every string discovered in `value` and apply `fn`. Returns\n * `{ output, matched }` where `output` is the rewritten payload and\n * `matched` is the de-duplicated list of pattern names that fired.\n *\n * The walker handles strings, plain objects, and arrays. Other JS\n * primitives are passed through untouched. Cycles are detected via a\n * `WeakSet` and broken with `'[Circular]'`.\n */\nfunction walk(\n value: unknown,\n fn: (s: string, matched: Set<string>) => string,\n matched: Set<string>,\n seen: WeakSet<object>,\n): unknown {\n if (typeof value === 'string') return fn(value, matched);\n if (value === null || typeof value !== 'object') return value;\n if (seen.has(value as object)) return '[Circular]';\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n const out = new Array<unknown>(value.length);\n for (let i = 0; i < value.length; i++) {\n out[i] = walk(value[i], fn, matched, seen);\n }\n return out;\n }\n\n const obj = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n out[key] = walk(obj[key], fn, matched, seen);\n }\n return out;\n}\n\n/**\n * Apply every active pattern to `s`. Mutates `matched` with the names\n * that fired and returns the rewritten string.\n */\nfunction applyPatterns(\n s: string,\n patterns: ReadonlyArray<RedactionPattern>,\n matched: Set<string>,\n): string {\n let out = s;\n for (const p of patterns) {\n const mask = p.mask ?? `[REDACTED ${p.name}]`;\n p.regex.lastIndex = 0;\n if (p.verify === undefined) {\n if (p.regex.test(out)) {\n matched.add(p.name);\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, mask);\n }\n continue;\n }\n // RP-21: per-match predicate - only mask hits the verifier accepts (e.g.\n // Luhn-valid PANs), and only count the pattern as matched when one did.\n const verify = p.verify;\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, (m) => {\n if (verify(m)) {\n matched.add(p.name);\n return mask;\n }\n return m;\n });\n }\n return out;\n}\n\nfunction violationFor(\n reason: RedactionViolation['reason'],\n input: RedactionInput,\n patterns?: ReadonlyArray<string>,\n): RedactionViolation {\n const baseContext = input.context;\n const base: RedactionViolation = {\n reason,\n declaredTier: input.tier,\n ...(baseContext?.attribute === undefined ? {} : { attribute: baseContext.attribute }),\n ...(baseContext?.spanType === undefined ? {} : { spanType: baseContext.spanType }),\n ...(baseContext?.origin === undefined ? {} : { origin: baseContext.origin }),\n ...(patterns === undefined || patterns.length === 0 ? {} : { patterns }),\n };\n return base;\n}\n\n/**\n * Create a {@link RedactionValidator} configured against the supplied\n * options. The result implements both the `RedactionValidator`\n * contract from `@graphorin/core` and the\n * {@link RedactionValidatorInstance} extension surface (counters +\n * reset).\n *\n * @stable\n */\nexport function createRedactionValidator(\n opts: RedactionValidatorOptions = {},\n): RedactionValidatorInstance {\n const id = opts.id ?? 'default';\n const minTier: Sensitivity = opts.minTier ?? 'public';\n const failOnUnredacted = opts.failOnUnredactedSensitive === true;\n const patterns = selectPatterns(\n opts.patterns ?? BUILT_IN_PATTERNS,\n opts.enabledPatterns,\n opts.disabledPatterns,\n );\n const onViolation = opts.onViolation;\n const custom = opts.customValidator;\n\n let counters = makeCounters();\n\n function recordViolation(violation: RedactionViolation): void {\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n for (const name of violation.patterns ?? []) {\n bump(counters.matchesByPattern, name);\n }\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n // Listener errors must never break the exporter.\n }\n }\n }\n\n function dropOrThrow(_input: RedactionInput, violation: RedactionViolation): null {\n recordViolation(violation);\n if (failOnUnredacted) {\n throw new RedactionValidationError(\n `RedactionValidator dropped value (${violation.reason}) - set ` +\n 'failOnUnredactedSensitive: false in production to keep the ' +\n 'pipeline running.',\n violation,\n );\n }\n return null;\n }\n\n const validator: RedactionValidator = {\n id,\n minTier,\n validate(input: RedactionInput): RedactionOutput | null {\n if (input === null || input === undefined || typeof input !== 'object') {\n return dropOrThrow(\n input as RedactionInput,\n violationFor('invalid-input', input as RedactionInput),\n );\n }\n\n const declared = input.tier ?? 'internal';\n\n if (rank(declared) > rank(minTier)) {\n return dropOrThrow(input, violationFor('sensitivity-tier-exceeded', input));\n }\n\n const matched = new Set<string>();\n const rewritten = walk(\n input.value,\n (s, m) => applyPatterns(s, patterns, m),\n matched,\n new WeakSet(),\n );\n\n if (matched.size > 0) {\n const matchedList = [...matched];\n for (const name of matchedList) {\n bump(counters.matchesByPattern, name);\n }\n const containsSecret = matchedList.some(\n (name) => patterns.find((p) => p.name === name)?.category === 'secret',\n );\n\n if (containsSecret) {\n if (failOnUnredacted) {\n const violation = violationFor('secret-pattern-match', input, matchedList);\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n /* listener errors swallowed */\n }\n }\n throw new RedactionValidationError(\n `RedactionValidator detected secret pattern(s) in attribute (${matchedList.join(', ')}).`,\n violation,\n );\n }\n }\n }\n\n const out: RedactionOutput =\n matched.size === 0\n ? { value: rewritten, tier: declared }\n : { value: rewritten, tier: declared, matched: [...matched] };\n\n if (custom !== undefined) {\n return custom({ ...input, value: rewritten });\n }\n\n return out;\n },\n };\n\n const instance: RedactionValidatorInstance = {\n ...validator,\n counters: () => freezeCounters(counters),\n resetCounters: () => {\n counters = makeCounters();\n },\n };\n\n return instance;\n}\n\n/**\n * Quickly compute the relative ordering of two sensitivity tiers.\n * Exposed because the tracer + replay layers need it without taking a\n * full dependency on the validator implementation.\n *\n * @stable\n */\nexport function compareSensitivityTiers(a: Sensitivity, b: Sensitivity): number {\n return rank(a) - rank(b);\n}\n"],"mappings":"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"}
@@ -26,7 +26,7 @@ interface ReplayLogConfig {
26
26
  /**
27
27
  * Auto-prune hint. `enabled` + `schedule` describe a daily prune of files
28
28
  * older than `retentionDays`, but **no built-in scheduler consumes this
29
- * yet** (RP-19) it is a declarative intent. Until a host wires it to a
29
+ * yet** (RP-19) - it is a declarative intent. Until a host wires it to a
30
30
  * trigger, callers must run `pruneTraces(...)` themselves; the default is
31
31
  * therefore `enabled: false` so the option is not an inert default-on.
32
32
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":["DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'>"],"sources":["../../src/replay/config.ts"],"sourcesContent":["/**\n * Configuration shapes for the replay log. The shapes mirror the\n * canonical `observability.replayLog.*` settings so consumer\n * configuration files can use a single typed structure.\n *\n * @packageDocumentation\n */\n\n/**\n * Shape consumed by `observability.replayLog.*`.\n *\n * @stable\n */\nexport interface ReplayLogConfig {\n /**\n * Root directory for the JSONL trace files. Required when the\n * replay log is enabled.\n */\n readonly path: string;\n /**\n * Retention window in days. `0` keeps every file forever.\n *\n * @default 30\n */\n readonly retentionDays?: number;\n /**\n * Auto-prune hint. `enabled` + `schedule` describe a daily prune of files\n * older than `retentionDays`, but **no built-in scheduler consumes this\n * yet** (RP-19) it is a declarative intent. Until a host wires it to a\n * trigger, callers must run `pruneTraces(...)` themselves; the default is\n * therefore `enabled: false` so the option is not an inert default-on.\n *\n * @default { enabled: false, schedule: '0 4 * * *' }\n */\n readonly autoPrune?: {\n readonly enabled: boolean;\n readonly schedule?: string;\n };\n /**\n * Encryption-at-rest toggle. `'off'` (default) writes plain JSONL;\n * the opt-in `'aes256gcm'` mode hooks into the encryption-at-rest\n * passphrase chain (Phase 16 deliverable).\n *\n * @default 'off'\n */\n readonly encryption?: 'off' | 'aes256gcm';\n}\n\n/**\n * Default values for the replay log configuration.\n *\n * @stable\n */\nexport const DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'> = Object.freeze({\n retentionDays: 30,\n autoPrune: Object.freeze({ enabled: false, schedule: '0 4 * * *' }),\n encryption: 'off' as const,\n});\n"],"mappings":";;;;;;AAqDA,MAAaA,4BAAqE,OAAO,OAAO;CAC9F,eAAe;CACf,WAAW,OAAO,OAAO;EAAE,SAAS;EAAO,UAAU;EAAa,CAAC;CACnE,YAAY;CACb,CAAC"}
1
+ {"version":3,"file":"config.js","names":["DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'>"],"sources":["../../src/replay/config.ts"],"sourcesContent":["/**\n * Configuration shapes for the replay log. The shapes mirror the\n * canonical `observability.replayLog.*` settings so consumer\n * configuration files can use a single typed structure.\n *\n * @packageDocumentation\n */\n\n/**\n * Shape consumed by `observability.replayLog.*`.\n *\n * @stable\n */\nexport interface ReplayLogConfig {\n /**\n * Root directory for the JSONL trace files. Required when the\n * replay log is enabled.\n */\n readonly path: string;\n /**\n * Retention window in days. `0` keeps every file forever.\n *\n * @default 30\n */\n readonly retentionDays?: number;\n /**\n * Auto-prune hint. `enabled` + `schedule` describe a daily prune of files\n * older than `retentionDays`, but **no built-in scheduler consumes this\n * yet** (RP-19) - it is a declarative intent. Until a host wires it to a\n * trigger, callers must run `pruneTraces(...)` themselves; the default is\n * therefore `enabled: false` so the option is not an inert default-on.\n *\n * @default { enabled: false, schedule: '0 4 * * *' }\n */\n readonly autoPrune?: {\n readonly enabled: boolean;\n readonly schedule?: string;\n };\n /**\n * Encryption-at-rest toggle. `'off'` (default) writes plain JSONL;\n * the opt-in `'aes256gcm'` mode hooks into the encryption-at-rest\n * passphrase chain (Phase 16 deliverable).\n *\n * @default 'off'\n */\n readonly encryption?: 'off' | 'aes256gcm';\n}\n\n/**\n * Default values for the replay log configuration.\n *\n * @stable\n */\nexport const DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'> = Object.freeze({\n retentionDays: 30,\n autoPrune: Object.freeze({ enabled: false, schedule: '0 4 * * *' }),\n encryption: 'off' as const,\n});\n"],"mappings":";;;;;;AAqDA,MAAaA,4BAAqE,OAAO,OAAO;CAC9F,eAAe;CACf,WAAW,OAAO,OAAO;EAAE,SAAS;EAAO,UAAU;EAAa,CAAC;CACnE,YAAY;CACb,CAAC"}
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
 
4
4
  //#region src/replay/log.ts
5
5
  /**
6
- * `getTraceLog(...)` / `pruneTraces(...)` minimal helpers for reading
6
+ * `getTraceLog(...)` / `pruneTraces(...)` - minimal helpers for reading
7
7
  * back the JSONL files produced by {@link createJSONLExporter} and
8
8
  * pruning old files based on retention policy.
9
9
  *
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","names":["removed: string[]"],"sources":["../../src/replay/log.ts"],"sourcesContent":["/**\n * `getTraceLog(...)` / `pruneTraces(...)` minimal helpers for reading\n * back the JSONL files produced by {@link createJSONLExporter} and\n * pruning old files based on retention policy.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, unlink } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nimport type { SpanRecord } from '../exporters/types.js';\n\n/**\n * Read every span record from a JSONL trace log. Lines that fail to parse are\n * skipped (the iterator keeps going); the generator only ever yields parsed\n * {@link SpanRecord} values, never `null`.\n *\n * @stable\n */\nexport async function* getTraceLog(filePath: string): AsyncIterable<SpanRecord> {\n const data = await readFile(filePath, 'utf8');\n for (const line of data.split('\\n')) {\n if (line.trim() === '') continue;\n try {\n const parsed = JSON.parse(line) as SpanRecord;\n yield parsed;\n } catch {\n // Skip malformed lines replay must keep going.\n }\n }\n}\n\n/**\n * Configuration shape for {@link pruneTraces}.\n *\n * @stable\n */\nexport interface PruneTracesOptions {\n /** Root directory housing the JSONL files. */\n readonly root: string;\n /** Files older than `olderThanDays` are deleted. `0` keeps every file. */\n readonly olderThanDays: number;\n /**\n * Wall clock used to compute the threshold. Defaults to `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/**\n * Remove every JSONL file that is older than the configured retention\n * window. Returns the deleted files for caller-side accounting.\n *\n * @stable\n */\nexport async function pruneTraces(opts: PruneTracesOptions): Promise<ReadonlyArray<string>> {\n if (opts.olderThanDays <= 0) return [];\n\n const now = opts.now ?? (() => Date.now());\n const threshold = now() - opts.olderThanDays * 24 * 60 * 60 * 1000;\n const root = resolve(opts.root);\n const removed: string[] = [];\n\n for await (const filePath of walkJsonl(root)) {\n const info = await stat(filePath);\n if (info.mtimeMs < threshold) {\n await unlink(filePath);\n removed.push(filePath);\n }\n }\n\n return removed;\n}\n\nasync function* walkJsonl(root: string): AsyncIterable<string> {\n const entries = await readdir(root, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(root, entry.name);\n if (entry.isDirectory()) {\n yield* walkJsonl(full);\n continue;\n }\n if (entry.isFile() && full.endsWith('.jsonl')) {\n yield full;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,gBAAuB,YAAY,UAA6C;CAC9E,MAAM,OAAO,MAAM,SAAS,UAAU,OAAO;AAC7C,MAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;AACnC,MAAI,KAAK,MAAM,KAAK,GAAI;AACxB,MAAI;AAEF,SADe,KAAK,MAAM,KAAK;UAEzB;;;;;;;;;AA8BZ,eAAsB,YAAY,MAA0D;AAC1F,KAAI,KAAK,iBAAiB,EAAG,QAAO,EAAE;CAGtC,MAAM,aADM,KAAK,cAAc,KAAK,KAAK,IAClB,GAAG,KAAK,gBAAgB,KAAK,KAAK,KAAK;CAC9D,MAAM,OAAO,QAAQ,KAAK,KAAK;CAC/B,MAAMA,UAAoB,EAAE;AAE5B,YAAW,MAAM,YAAY,UAAU,KAAK,CAE1C,MADa,MAAM,KAAK,SAAS,EACxB,UAAU,WAAW;AAC5B,QAAM,OAAO,SAAS;AACtB,UAAQ,KAAK,SAAS;;AAI1B,QAAO;;AAGT,gBAAgB,UAAU,MAAqC;CAC7D,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,MAAM,CAAC;AAC5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,OAAO,KAAK,MAAM,MAAM,KAAK;AACnC,MAAI,MAAM,aAAa,EAAE;AACvB,UAAO,UAAU,KAAK;AACtB;;AAEF,MAAI,MAAM,QAAQ,IAAI,KAAK,SAAS,SAAS,CAC3C,OAAM"}
1
+ {"version":3,"file":"log.js","names":["removed: string[]"],"sources":["../../src/replay/log.ts"],"sourcesContent":["/**\n * `getTraceLog(...)` / `pruneTraces(...)` - minimal helpers for reading\n * back the JSONL files produced by {@link createJSONLExporter} and\n * pruning old files based on retention policy.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, unlink } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nimport type { SpanRecord } from '../exporters/types.js';\n\n/**\n * Read every span record from a JSONL trace log. Lines that fail to parse are\n * skipped (the iterator keeps going); the generator only ever yields parsed\n * {@link SpanRecord} values, never `null`.\n *\n * @stable\n */\nexport async function* getTraceLog(filePath: string): AsyncIterable<SpanRecord> {\n const data = await readFile(filePath, 'utf8');\n for (const line of data.split('\\n')) {\n if (line.trim() === '') continue;\n try {\n const parsed = JSON.parse(line) as SpanRecord;\n yield parsed;\n } catch {\n // Skip malformed lines - replay must keep going.\n }\n }\n}\n\n/**\n * Configuration shape for {@link pruneTraces}.\n *\n * @stable\n */\nexport interface PruneTracesOptions {\n /** Root directory housing the JSONL files. */\n readonly root: string;\n /** Files older than `olderThanDays` are deleted. `0` keeps every file. */\n readonly olderThanDays: number;\n /**\n * Wall clock used to compute the threshold. Defaults to `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/**\n * Remove every JSONL file that is older than the configured retention\n * window. Returns the deleted files for caller-side accounting.\n *\n * @stable\n */\nexport async function pruneTraces(opts: PruneTracesOptions): Promise<ReadonlyArray<string>> {\n if (opts.olderThanDays <= 0) return [];\n\n const now = opts.now ?? (() => Date.now());\n const threshold = now() - opts.olderThanDays * 24 * 60 * 60 * 1000;\n const root = resolve(opts.root);\n const removed: string[] = [];\n\n for await (const filePath of walkJsonl(root)) {\n const info = await stat(filePath);\n if (info.mtimeMs < threshold) {\n await unlink(filePath);\n removed.push(filePath);\n }\n }\n\n return removed;\n}\n\nasync function* walkJsonl(root: string): AsyncIterable<string> {\n const entries = await readdir(root, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(root, entry.name);\n if (entry.isDirectory()) {\n yield* walkJsonl(full);\n continue;\n }\n if (entry.isFile() && full.endsWith('.jsonl')) {\n yield full;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,gBAAuB,YAAY,UAA6C;CAC9E,MAAM,OAAO,MAAM,SAAS,UAAU,OAAO;AAC7C,MAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;AACnC,MAAI,KAAK,MAAM,KAAK,GAAI;AACxB,MAAI;AAEF,SADe,KAAK,MAAM,KAAK;UAEzB;;;;;;;;;AA8BZ,eAAsB,YAAY,MAA0D;AAC1F,KAAI,KAAK,iBAAiB,EAAG,QAAO,EAAE;CAGtC,MAAM,aADM,KAAK,cAAc,KAAK,KAAK,IAClB,GAAG,KAAK,gBAAgB,KAAK,KAAK,KAAK;CAC9D,MAAM,OAAO,QAAQ,KAAK,KAAK;CAC/B,MAAMA,UAAoB,EAAE;AAE5B,YAAW,MAAM,YAAY,UAAU,KAAK,CAE1C,MADa,MAAM,KAAK,SAAS,EACxB,UAAU,WAAW;AAC5B,QAAM,OAAO,SAAS;AACtB,UAAQ,KAAK,SAAS;;AAI1B,QAAO;;AAGT,gBAAgB,UAAU,MAAqC;CAC7D,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,MAAM,CAAC;AAC5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,OAAO,KAAK,MAAM,MAAM,KAAK;AACnC,MAAI,MAAM,aAAa,EAAE;AACvB,UAAO,UAAU,KAAK;AACtB;;AAEF,MAAI,MAAM,QAAQ,IAAI,KAAK,SAAS,SAAS,CAC3C,OAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"replay.js","names":["DEFAULT_ACTOR: ReplayAuditEvent['actor']","validator: RedactionValidatorInstance","audit: ReplayAuditBridge | undefined","minSensitivity: Sensitivity"],"sources":["../../src/replay/replay.ts"],"sourcesContent":["/**\n * `createReplay(...)` sanitized-by-default replay primitives.\n *\n * Replay never opens the underlying file directly; callers feed it an\n * iterable of `SpanRecord`s. The {@link createTraceLogReader} helper\n * (file-backed JSONL) and `getTraceLog(...)` aggregate utility live\n * alongside this module.\n *\n * @packageDocumentation\n */\n\nimport type { Sensitivity } from '@graphorin/core';\n\nimport type { SpanRecord } from '../exporters/types.js';\nimport { sanitizeRecord } from '../exporters/with-validation.js';\nimport type { RedactionValidatorInstance } from '../redaction/types.js';\nimport { compareSensitivityTiers, createRedactionValidator } from '../redaction/validator.js';\n\nimport type {\n ReplayAuditBridge,\n ReplayAuditEvent,\n ReplayEvent,\n ReplayOptions,\n ReplayRunInput,\n} from './types.js';\n\nconst DEFAULT_VALIDATOR = createRedactionValidator({ minTier: 'public' });\nconst DEFAULT_ACTOR: ReplayAuditEvent['actor'] = { kind: 'system', id: 'in-process' };\n\n/**\n * @stable\n */\nexport interface Replay {\n run(input: ReplayRunInput): AsyncIterable<ReplayEvent>;\n}\n\n/**\n * Build a replay primitive. The returned object exposes a single\n * `run(...)` async iterator that yields {@link ReplayEvent} records.\n *\n * Sanitized mode is the default and applies the configured\n * {@link RedactionValidatorInstance} to every record. Raw mode\n * requires the `canReadRaw` callback to return `true` AND emits an\n * audit log entry on every invocation.\n *\n * @stable\n */\nexport function createReplay(opts: ReplayOptions = {}): Replay {\n const validator: RedactionValidatorInstance = opts.validator ?? DEFAULT_VALIDATOR;\n const audit: ReplayAuditBridge | undefined = opts.audit;\n const defaultActor = opts.defaultActor ?? DEFAULT_ACTOR;\n const canReadRaw = opts.canReadRaw ?? (() => true);\n\n return {\n async *run(input: ReplayRunInput): AsyncIterable<ReplayEvent> {\n const mode = input.mode ?? 'sanitized';\n const minSensitivity: Sensitivity = input.minSensitivity ?? 'public';\n const actor = input.actor ?? defaultActor;\n const start = Date.now();\n\n if (mode === 'raw' && !canReadRaw({ target: input.target })) {\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'denied',\n metadata: { mode, minSensitivity, eventCount: 0, durationMs: Date.now() - start },\n });\n yield { type: 'replay.start', target: input.target, mode };\n yield { type: 'replay.skipped', reason: 'access-denied', spanId: input.target };\n yield {\n type: 'replay.end',\n durationMs: Date.now() - start,\n eventsEmitted: 0,\n eventsSkipped: 1,\n };\n return;\n }\n\n let emitted = 0;\n let skipped = 0;\n let started = false;\n let reachedFrom = input.fromSpanId === undefined;\n\n const iterator = isAsyncIterable(input.source)\n ? input.source[Symbol.asyncIterator]()\n : asAsync(input.source[Symbol.iterator]());\n\n try {\n while (true) {\n const next = await iterator.next();\n if (next.done === true) break;\n const record = next.value;\n\n if (!started) {\n started = true;\n yield { type: 'replay.start', target: input.target, mode };\n }\n\n if (!reachedFrom) {\n if (record.id === input.fromSpanId) reachedFrom = true;\n else continue;\n }\n\n if (mode === 'raw') {\n yield { type: 'replay.event', span: record, sanitized: false };\n emitted += 1;\n continue;\n }\n\n if (skipBySensitivity(record, minSensitivity)) {\n skipped += 1;\n yield { type: 'replay.skipped', reason: 'sensitivity', spanId: record.id };\n continue;\n }\n\n const sanitized = sanitizeRecord(record, validator);\n // RP-18: sanitization is attribute-granular and never drops the whole\n // span. A record arrives pre-flagged with `droppedReason` only if an\n // upstream stage marked it; honour that, otherwise replay the\n // stripped span.\n if (sanitized.droppedReason !== undefined) {\n skipped += 1;\n yield {\n type: 'replay.skipped',\n reason: 'redaction-violation',\n spanId: record.id,\n };\n continue;\n }\n\n yield { type: 'replay.event', span: sanitized, sanitized: true };\n emitted += 1;\n }\n } finally {\n const durationMs = Date.now() - start;\n if (!started) {\n yield { type: 'replay.start', target: input.target, mode };\n }\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'success',\n metadata: {\n mode,\n minSensitivity,\n ...(input.fromSpanId === undefined ? {} : { fromSpanId: input.fromSpanId }),\n eventCount: emitted,\n durationMs,\n },\n });\n yield {\n type: 'replay.end',\n durationMs,\n eventsEmitted: emitted,\n eventsSkipped: skipped,\n };\n }\n },\n };\n}\n\nfunction emitAudit(bridge: ReplayAuditBridge | undefined, event: ReplayAuditEvent): void {\n if (bridge === undefined) return;\n try {\n bridge.emit(event);\n } catch {\n // Audit listener errors must never break the replay pipeline.\n }\n}\n\nfunction skipBySensitivity(record: SpanRecord, floor: Sensitivity): boolean {\n if (record.sensitivityByAttribute === undefined) return false;\n for (const value of Object.values(record.sensitivityByAttribute)) {\n if (typeof value === 'string') {\n const tier = value as Sensitivity;\n if (tier === 'public' || tier === 'internal' || tier === 'secret') {\n if (compareSensitivityTiers(tier, floor) > 0) return true;\n }\n }\n }\n return false;\n}\n\nfunction isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {\n return value !== null && typeof value === 'object' && Symbol.asyncIterator in (value as object);\n}\n\nfunction asAsync<T>(it: Iterator<T>): AsyncIterator<T> {\n return {\n next(): Promise<IteratorResult<T>> {\n return Promise.resolve(it.next());\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"replay.js","names":["DEFAULT_ACTOR: ReplayAuditEvent['actor']","validator: RedactionValidatorInstance","audit: ReplayAuditBridge | undefined","minSensitivity: Sensitivity"],"sources":["../../src/replay/replay.ts"],"sourcesContent":["/**\n * `createReplay(...)` - sanitized-by-default replay primitives.\n *\n * Replay never opens the underlying file directly; callers feed it an\n * iterable of `SpanRecord`s. The {@link createTraceLogReader} helper\n * (file-backed JSONL) and `getTraceLog(...)` aggregate utility live\n * alongside this module.\n *\n * @packageDocumentation\n */\n\nimport type { Sensitivity } from '@graphorin/core';\n\nimport type { SpanRecord } from '../exporters/types.js';\nimport { sanitizeRecord } from '../exporters/with-validation.js';\nimport type { RedactionValidatorInstance } from '../redaction/types.js';\nimport { compareSensitivityTiers, createRedactionValidator } from '../redaction/validator.js';\n\nimport type {\n ReplayAuditBridge,\n ReplayAuditEvent,\n ReplayEvent,\n ReplayOptions,\n ReplayRunInput,\n} from './types.js';\n\nconst DEFAULT_VALIDATOR = createRedactionValidator({ minTier: 'public' });\nconst DEFAULT_ACTOR: ReplayAuditEvent['actor'] = { kind: 'system', id: 'in-process' };\n\n/**\n * @stable\n */\nexport interface Replay {\n run(input: ReplayRunInput): AsyncIterable<ReplayEvent>;\n}\n\n/**\n * Build a replay primitive. The returned object exposes a single\n * `run(...)` async iterator that yields {@link ReplayEvent} records.\n *\n * Sanitized mode is the default and applies the configured\n * {@link RedactionValidatorInstance} to every record. Raw mode\n * requires the `canReadRaw` callback to return `true` AND emits an\n * audit log entry on every invocation.\n *\n * @stable\n */\nexport function createReplay(opts: ReplayOptions = {}): Replay {\n const validator: RedactionValidatorInstance = opts.validator ?? DEFAULT_VALIDATOR;\n const audit: ReplayAuditBridge | undefined = opts.audit;\n const defaultActor = opts.defaultActor ?? DEFAULT_ACTOR;\n const canReadRaw = opts.canReadRaw ?? (() => true);\n\n return {\n async *run(input: ReplayRunInput): AsyncIterable<ReplayEvent> {\n const mode = input.mode ?? 'sanitized';\n const minSensitivity: Sensitivity = input.minSensitivity ?? 'public';\n const actor = input.actor ?? defaultActor;\n const start = Date.now();\n\n if (mode === 'raw' && !canReadRaw({ target: input.target })) {\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'denied',\n metadata: { mode, minSensitivity, eventCount: 0, durationMs: Date.now() - start },\n });\n yield { type: 'replay.start', target: input.target, mode };\n yield { type: 'replay.skipped', reason: 'access-denied', spanId: input.target };\n yield {\n type: 'replay.end',\n durationMs: Date.now() - start,\n eventsEmitted: 0,\n eventsSkipped: 1,\n };\n return;\n }\n\n let emitted = 0;\n let skipped = 0;\n let started = false;\n let reachedFrom = input.fromSpanId === undefined;\n\n const iterator = isAsyncIterable(input.source)\n ? input.source[Symbol.asyncIterator]()\n : asAsync(input.source[Symbol.iterator]());\n\n try {\n while (true) {\n const next = await iterator.next();\n if (next.done === true) break;\n const record = next.value;\n\n if (!started) {\n started = true;\n yield { type: 'replay.start', target: input.target, mode };\n }\n\n if (!reachedFrom) {\n if (record.id === input.fromSpanId) reachedFrom = true;\n else continue;\n }\n\n if (mode === 'raw') {\n yield { type: 'replay.event', span: record, sanitized: false };\n emitted += 1;\n continue;\n }\n\n if (skipBySensitivity(record, minSensitivity)) {\n skipped += 1;\n yield { type: 'replay.skipped', reason: 'sensitivity', spanId: record.id };\n continue;\n }\n\n const sanitized = sanitizeRecord(record, validator);\n // RP-18: sanitization is attribute-granular and never drops the whole\n // span. A record arrives pre-flagged with `droppedReason` only if an\n // upstream stage marked it; honour that, otherwise replay the\n // stripped span.\n if (sanitized.droppedReason !== undefined) {\n skipped += 1;\n yield {\n type: 'replay.skipped',\n reason: 'redaction-violation',\n spanId: record.id,\n };\n continue;\n }\n\n yield { type: 'replay.event', span: sanitized, sanitized: true };\n emitted += 1;\n }\n } finally {\n const durationMs = Date.now() - start;\n if (!started) {\n yield { type: 'replay.start', target: input.target, mode };\n }\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'success',\n metadata: {\n mode,\n minSensitivity,\n ...(input.fromSpanId === undefined ? {} : { fromSpanId: input.fromSpanId }),\n eventCount: emitted,\n durationMs,\n },\n });\n yield {\n type: 'replay.end',\n durationMs,\n eventsEmitted: emitted,\n eventsSkipped: skipped,\n };\n }\n },\n };\n}\n\nfunction emitAudit(bridge: ReplayAuditBridge | undefined, event: ReplayAuditEvent): void {\n if (bridge === undefined) return;\n try {\n bridge.emit(event);\n } catch {\n // Audit listener errors must never break the replay pipeline.\n }\n}\n\nfunction skipBySensitivity(record: SpanRecord, floor: Sensitivity): boolean {\n if (record.sensitivityByAttribute === undefined) return false;\n for (const value of Object.values(record.sensitivityByAttribute)) {\n if (typeof value === 'string') {\n const tier = value as Sensitivity;\n if (tier === 'public' || tier === 'internal' || tier === 'secret') {\n if (compareSensitivityTiers(tier, floor) > 0) return true;\n }\n }\n }\n return false;\n}\n\nfunction isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {\n return value !== null && typeof value === 'object' && Symbol.asyncIterator in (value as object);\n}\n\nfunction asAsync<T>(it: Iterator<T>): AsyncIterator<T> {\n return {\n next(): Promise<IteratorResult<T>> {\n return Promise.resolve(it.next());\n },\n };\n}\n"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { Sensitivity } from "@graphorin/core";
15
15
  type ReplayMode = 'sanitized' | 'raw';
16
16
  /**
17
17
  * Audit-bridge contract used by the replay layer. Every replay
18
- * invocation emits one entry through the bridge sanitized + raw
18
+ * invocation emits one entry through the bridge - sanitized + raw
19
19
  * alike. The actual audit storage lives in `@graphorin/security`; the
20
20
  * replay layer keeps the bridge generic so the package stays free of
21
21
  * a hard dependency on the security package.
@@ -78,7 +78,7 @@ type ReplayEvent = {
78
78
  interface ReplayOptions {
79
79
  /** Validator used to sanitize records when `mode === 'sanitized'`. */
80
80
  readonly validator?: RedactionValidatorInstance;
81
- /** Optional audit bridge called once per replay invocation. */
81
+ /** Optional audit bridge - called once per replay invocation. */
82
82
  readonly audit?: ReplayAuditBridge;
83
83
  /** Default actor reported via `audit.actor` when none is supplied. */
84
84
  readonly defaultActor?: ReplayAuditEvent['actor'];
@@ -38,8 +38,8 @@ function announceTelemetryPosture(opts = {}) {
38
38
  const env = opts.env ?? process.env;
39
39
  const sink = opts.sink ?? ((line) => console.info(line));
40
40
  const lines = [];
41
- if (env.GRAPHORIN_TELEMETRY !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} acknowledged. Telemetry is hardcoded \`disabled\` in v0.1.`);
42
- if (env.GRAPHORIN_NO_PHONE_HOME !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} acknowledged. Graphorin already makes zero outbound calls without explicit user action.`);
41
+ if (env.GRAPHORIN_TELEMETRY !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} - acknowledged. Telemetry is hardcoded \`disabled\` in v0.1.`);
42
+ if (env.GRAPHORIN_NO_PHONE_HOME !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} - acknowledged. Graphorin already makes zero outbound calls without explicit user action.`);
43
43
  for (const line of lines) sink(line);
44
44
  return lines;
45
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["lines: string[]"],"sources":["../../src/telemetry/index.ts"],"sourcesContent":["/**\n * Zero-default telemetry stub.\n *\n * The framework promises to make zero outbound network calls without\n * an explicit user action. This module exists so consumers can:\n *\n * - inspect the telemetry posture from CLI / health endpoints,\n * - register the reserved `GRAPHORIN_TELEMETRY` / `GRAPHORIN_NO_PHONE_HOME`\n * environment variables for forward compatibility,\n * - assert in tests that no implicit telemetry surface exists.\n *\n * Attempting to enable telemetry pre-v0.2 returns a sentinel result; it\n * never opens a socket.\n *\n * @packageDocumentation\n */\n\n/**\n * Result returned by {@link getTelemetryStatus}.\n *\n * @stable\n */\nexport interface TelemetryStatus {\n /** Always `false` in v0.1. Reserved field. */\n readonly enabled: false;\n /** Plain-English explanation of the current state. */\n readonly reason: string;\n /** Resolved value of `GRAPHORIN_TELEMETRY` (if any). */\n readonly env?: string;\n /** Resolved value of `GRAPHORIN_NO_PHONE_HOME` (if any). */\n readonly noPhoneHome?: string;\n}\n\n/**\n * Snapshot of the telemetry posture. Reads from `process.env` once\n * unless `env` is provided.\n *\n * @stable\n */\nexport function getTelemetryStatus(env: NodeJS.ProcessEnv = process.env): TelemetryStatus {\n const telemetry = env.GRAPHORIN_TELEMETRY;\n const noPhoneHome = env.GRAPHORIN_NO_PHONE_HOME;\n return {\n enabled: false,\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n ...(telemetry === undefined ? {} : { env: telemetry }),\n ...(noPhoneHome === undefined ? {} : { noPhoneHome }),\n };\n}\n\n/**\n * Best-effort enable hook. Always returns the sentinel\n * `{ status: 'disabled', reason: ... }` payload. Reserved for v0.2+.\n *\n * @stable\n */\nexport function enableTelemetry(): {\n readonly status: 'disabled';\n readonly reason: string;\n} {\n return {\n status: 'disabled',\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n };\n}\n\n/**\n * Detect the reserved env vars and emit one informational line per\n * process. Returns the lines as an array so callers can route them to\n * any sink they like (defaults to `console.info`).\n *\n * @stable\n */\nexport function announceTelemetryPosture(\n opts: { readonly env?: NodeJS.ProcessEnv; readonly sink?: (line: string) => void } = {},\n): ReadonlyArray<string> {\n const env = opts.env ?? process.env;\n const sink = opts.sink ?? ((line: string) => console.info(line));\n const lines: string[] = [];\n if (env.GRAPHORIN_TELEMETRY !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} ` +\n ' acknowledged. Telemetry is hardcoded `disabled` in v0.1.',\n );\n }\n if (env.GRAPHORIN_NO_PHONE_HOME !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} ` +\n ' acknowledged. Graphorin already makes zero outbound calls without explicit user action.',\n );\n }\n for (const line of lines) sink(line);\n return lines;\n}\n"],"mappings":";;;;;;;AAuCA,SAAgB,mBAAmB,MAAyB,QAAQ,KAAsB;CACxF,MAAM,YAAY,IAAI;CACtB,MAAM,cAAc,IAAI;AACxB,QAAO;EACL,SAAS;EACT,QAAQ;EACR,GAAI,cAAc,SAAY,EAAE,GAAG,EAAE,KAAK,WAAW;EACrD,GAAI,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa;EACrD;;;;;;;;AASH,SAAgB,kBAGd;AACA,QAAO;EACL,QAAQ;EACR,QAAQ;EACT;;;;;;;;;AAUH,SAAgB,yBACd,OAAqF,EAAE,EAChE;CACvB,MAAM,MAAM,KAAK,OAAO,QAAQ;CAChC,MAAM,OAAO,KAAK,UAAU,SAAiB,QAAQ,KAAK,KAAK;CAC/D,MAAMA,QAAkB,EAAE;AAC1B,KAAI,IAAI,wBAAwB,OAC9B,OAAM,KACJ,uDAAuD,IAAI,oBAAoB,+DAEhF;AAEH,KAAI,IAAI,4BAA4B,OAClC,OAAM,KACJ,2DAA2D,IAAI,wBAAwB,4FAExF;AAEH,MAAK,MAAM,QAAQ,MAAO,MAAK,KAAK;AACpC,QAAO"}
1
+ {"version":3,"file":"index.js","names":["lines: string[]"],"sources":["../../src/telemetry/index.ts"],"sourcesContent":["/**\n * Zero-default telemetry stub.\n *\n * The framework promises to make zero outbound network calls without\n * an explicit user action. This module exists so consumers can:\n *\n * - inspect the telemetry posture from CLI / health endpoints,\n * - register the reserved `GRAPHORIN_TELEMETRY` / `GRAPHORIN_NO_PHONE_HOME`\n * environment variables for forward compatibility,\n * - assert in tests that no implicit telemetry surface exists.\n *\n * Attempting to enable telemetry pre-v0.2 returns a sentinel result; it\n * never opens a socket.\n *\n * @packageDocumentation\n */\n\n/**\n * Result returned by {@link getTelemetryStatus}.\n *\n * @stable\n */\nexport interface TelemetryStatus {\n /** Always `false` in v0.1. Reserved field. */\n readonly enabled: false;\n /** Plain-English explanation of the current state. */\n readonly reason: string;\n /** Resolved value of `GRAPHORIN_TELEMETRY` (if any). */\n readonly env?: string;\n /** Resolved value of `GRAPHORIN_NO_PHONE_HOME` (if any). */\n readonly noPhoneHome?: string;\n}\n\n/**\n * Snapshot of the telemetry posture. Reads from `process.env` once\n * unless `env` is provided.\n *\n * @stable\n */\nexport function getTelemetryStatus(env: NodeJS.ProcessEnv = process.env): TelemetryStatus {\n const telemetry = env.GRAPHORIN_TELEMETRY;\n const noPhoneHome = env.GRAPHORIN_NO_PHONE_HOME;\n return {\n enabled: false,\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n ...(telemetry === undefined ? {} : { env: telemetry }),\n ...(noPhoneHome === undefined ? {} : { noPhoneHome }),\n };\n}\n\n/**\n * Best-effort enable hook. Always returns the sentinel\n * `{ status: 'disabled', reason: ... }` payload. Reserved for v0.2+.\n *\n * @stable\n */\nexport function enableTelemetry(): {\n readonly status: 'disabled';\n readonly reason: string;\n} {\n return {\n status: 'disabled',\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n };\n}\n\n/**\n * Detect the reserved env vars and emit one informational line per\n * process. Returns the lines as an array so callers can route them to\n * any sink they like (defaults to `console.info`).\n *\n * @stable\n */\nexport function announceTelemetryPosture(\n opts: { readonly env?: NodeJS.ProcessEnv; readonly sink?: (line: string) => void } = {},\n): ReadonlyArray<string> {\n const env = opts.env ?? process.env;\n const sink = opts.sink ?? ((line: string) => console.info(line));\n const lines: string[] = [];\n if (env.GRAPHORIN_TELEMETRY !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} ` +\n ' - acknowledged. Telemetry is hardcoded `disabled` in v0.1.',\n );\n }\n if (env.GRAPHORIN_NO_PHONE_HOME !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} ` +\n ' - acknowledged. Graphorin already makes zero outbound calls without explicit user action.',\n );\n }\n for (const line of lines) sink(line);\n return lines;\n}\n"],"mappings":";;;;;;;AAuCA,SAAgB,mBAAmB,MAAyB,QAAQ,KAAsB;CACxF,MAAM,YAAY,IAAI;CACtB,MAAM,cAAc,IAAI;AACxB,QAAO;EACL,SAAS;EACT,QAAQ;EACR,GAAI,cAAc,SAAY,EAAE,GAAG,EAAE,KAAK,WAAW;EACrD,GAAI,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa;EACrD;;;;;;;;AASH,SAAgB,kBAGd;AACA,QAAO;EACL,QAAQ;EACR,QAAQ;EACT;;;;;;;;;AAUH,SAAgB,yBACd,OAAqF,EAAE,EAChE;CACvB,MAAM,MAAM,KAAK,OAAO,QAAQ;CAChC,MAAM,OAAO,KAAK,UAAU,SAAiB,QAAQ,KAAK,KAAK;CAC/D,MAAMA,QAAkB,EAAE;AAC1B,KAAI,IAAI,wBAAwB,OAC9B,OAAM,KACJ,uDAAuD,IAAI,oBAAoB,gEAEhF;AAEH,KAAI,IAAI,4BAA4B,OAClC,OAAM,KACJ,2DAA2D,IAAI,wBAAwB,6FAExF;AAEH,MAAK,MAAM,QAAQ,MAAO,MAAK,KAAK;AACpC,QAAO"}
@@ -40,7 +40,7 @@ interface SamplingOptions {
40
40
  readonly now?: () => number;
41
41
  /**
42
42
  * Optional override for streaming-event sampling.
43
- * @see RB-52 streaming event family `tool.execute.{progress,partial}`.
43
+ * @see RB-52 - streaming event family `tool.execute.{progress,partial}`.
44
44
  */
45
45
  readonly streaming?: {
46
46
  readonly eventSamplingRate?: number;
@@ -66,7 +66,7 @@ interface Sampler {
66
66
  }
67
67
  /**
68
68
  * Build a {@link Sampler} from the supplied options. The sampler is
69
- * intentionally inexpensive every decision boils down to a single
69
+ * intentionally inexpensive - every decision boils down to a single
70
70
  * `random < threshold` comparison.
71
71
  *
72
72
  * @stable
@@ -1,7 +1,7 @@
1
1
  //#region src/tracer/sampling.ts
2
2
  /**
3
3
  * Build a {@link Sampler} from the supplied options. The sampler is
4
- * intentionally inexpensive every decision boils down to a single
4
+ * intentionally inexpensive - every decision boils down to a single
5
5
  * `random < threshold` comparison.
6
6
  *
7
7
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"sampling.js","names":["decisionMaker: SamplingDecisionMaker","windowStart: number | undefined"],"sources":["../../src/tracer/sampling.ts"],"sourcesContent":["/**\n * Head + tail sampling helpers. The tracer pairs the configured\n * sampler with a per-type override registry so chatty span types\n * (`memory.embed`, `tool.execute.partial`, …) can be downsampled\n * without affecting the rest of the trace stream.\n *\n * @packageDocumentation\n */\n\nimport type { SpanType } from '@graphorin/core';\n\n/** @stable */\nexport type SamplingDecisionMaker = 'parent-based' | 'always-on' | 'rate-limit';\n\n/**\n * Per-span-type rate override.\n *\n * @stable\n */\nexport interface SamplingRule {\n readonly type: SpanType | string;\n readonly rate: number;\n}\n\n/**\n * Configuration shape consumed by {@link createSampler}.\n *\n * @stable\n */\nexport interface SamplingOptions {\n /** Default head-sampling rate. Must be in `[0, 1]`. Defaults to `1.0`. */\n readonly rate?: number;\n /** Per-type overrides. Last write wins on duplicate `type`. */\n readonly rules?: ReadonlyArray<SamplingRule>;\n /** Decision maker. Defaults to `'parent-based'`. */\n readonly decisionMaker?: SamplingDecisionMaker;\n /**\n * Cap for the `'rate-limit'` decision maker: at most this many root spans\n * are sampled per rolling 1-second window (RP-19). `undefined` ⇒ no cap\n * (samples everything); `0` ⇒ sample nothing. Ignored by the other\n * decision makers.\n */\n readonly maxPerSecond?: number;\n /**\n * Clock for the `'rate-limit'` window. Defaults to `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n /**\n * Optional override for streaming-event sampling.\n * @see RB-52 streaming event family `tool.execute.{progress,partial}`.\n */\n readonly streaming?: {\n readonly eventSamplingRate?: number;\n readonly includeChunkContent?: 'none' | 'text-only' | 'all';\n };\n /**\n * Override for the random source. Useful for deterministic tests.\n *\n * @internal\n */\n readonly random?: () => number;\n}\n\n/**\n * @stable\n */\nexport interface Sampler {\n /** Decide whether a span of the given type should be recorded. */\n shouldSample(type: SpanType | string, parentSampled?: boolean): boolean;\n /** Decide whether a span event of the given name should be recorded. */\n shouldRecordEvent(name: string): boolean;\n /** Returns whether chunk *content* should travel through the exporter. */\n shouldIncludeChunkContent(): boolean;\n}\n\n/**\n * Build a {@link Sampler} from the supplied options. The sampler is\n * intentionally inexpensive every decision boils down to a single\n * `random < threshold` comparison.\n *\n * @stable\n */\nexport function createSampler(opts: SamplingOptions = {}): Sampler {\n const baseRate = clampRate(opts.rate ?? 1.0);\n const decisionMaker: SamplingDecisionMaker = opts.decisionMaker ?? 'parent-based';\n const ruleByType = new Map<string, number>();\n for (const rule of opts.rules ?? []) {\n ruleByType.set(rule.type, clampRate(rule.rate));\n }\n const streamingRate = clampRate(opts.streaming?.eventSamplingRate ?? 1.0);\n const includeChunks = opts.streaming?.includeChunkContent ?? 'none';\n const random = opts.random ?? Math.random;\n const maxPerSecond = opts.maxPerSecond;\n const now = opts.now ?? Date.now;\n let windowStart: number | undefined;\n let windowCount = 0;\n\n return {\n shouldSample(type, parentSampled): boolean {\n if (decisionMaker === 'always-on') return true;\n if (decisionMaker === 'parent-based') {\n // RP-19: true parent-based follow the parent's real sampling\n // decision. A child of an unsampled parent is NOT recorded (it would\n // otherwise be an orphan); a root span (no parent) falls through to\n // the rate.\n if (parentSampled === true) return true;\n if (parentSampled === false) return false;\n }\n if (decisionMaker === 'rate-limit') {\n // RP-19: a real token-window limiter, distinct from the probabilistic\n // path. Caps sampled spans to `maxPerSecond` per rolling second.\n if (maxPerSecond === undefined) return true;\n if (maxPerSecond <= 0) return false;\n const t = now();\n if (windowStart === undefined || t - windowStart >= 1000) {\n windowStart = t;\n windowCount = 0;\n }\n if (windowCount < maxPerSecond) {\n windowCount += 1;\n return true;\n }\n return false;\n }\n const rate = ruleByType.get(type) ?? baseRate;\n if (rate >= 1) return true;\n if (rate <= 0) return false;\n return random() < rate;\n },\n shouldRecordEvent(name): boolean {\n const rate = ruleByType.get(name) ?? streamingRate;\n if (rate >= 1) return true;\n if (rate <= 0) return false;\n return random() < rate;\n },\n shouldIncludeChunkContent(): boolean {\n return includeChunks !== 'none';\n },\n };\n}\n\nfunction clampRate(rate: number): number {\n if (Number.isNaN(rate)) return 1;\n if (rate < 0) return 0;\n if (rate > 1) return 1;\n return rate;\n}\n"],"mappings":";;;;;;;;AAoFA,SAAgB,cAAc,OAAwB,EAAE,EAAW;CACjE,MAAM,WAAW,UAAU,KAAK,QAAQ,EAAI;CAC5C,MAAMA,gBAAuC,KAAK,iBAAiB;CACnE,MAAM,6BAAa,IAAI,KAAqB;AAC5C,MAAK,MAAM,QAAQ,KAAK,SAAS,EAAE,CACjC,YAAW,IAAI,KAAK,MAAM,UAAU,KAAK,KAAK,CAAC;CAEjD,MAAM,gBAAgB,UAAU,KAAK,WAAW,qBAAqB,EAAI;CACzE,MAAM,gBAAgB,KAAK,WAAW,uBAAuB;CAC7D,MAAM,SAAS,KAAK,UAAU,KAAK;CACnC,MAAM,eAAe,KAAK;CAC1B,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,IAAIC;CACJ,IAAI,cAAc;AAElB,QAAO;EACL,aAAa,MAAM,eAAwB;AACzC,OAAI,kBAAkB,YAAa,QAAO;AAC1C,OAAI,kBAAkB,gBAAgB;AAKpC,QAAI,kBAAkB,KAAM,QAAO;AACnC,QAAI,kBAAkB,MAAO,QAAO;;AAEtC,OAAI,kBAAkB,cAAc;AAGlC,QAAI,iBAAiB,OAAW,QAAO;AACvC,QAAI,gBAAgB,EAAG,QAAO;IAC9B,MAAM,IAAI,KAAK;AACf,QAAI,gBAAgB,UAAa,IAAI,eAAe,KAAM;AACxD,mBAAc;AACd,mBAAc;;AAEhB,QAAI,cAAc,cAAc;AAC9B,oBAAe;AACf,YAAO;;AAET,WAAO;;GAET,MAAM,OAAO,WAAW,IAAI,KAAK,IAAI;AACrC,OAAI,QAAQ,EAAG,QAAO;AACtB,OAAI,QAAQ,EAAG,QAAO;AACtB,UAAO,QAAQ,GAAG;;EAEpB,kBAAkB,MAAe;GAC/B,MAAM,OAAO,WAAW,IAAI,KAAK,IAAI;AACrC,OAAI,QAAQ,EAAG,QAAO;AACtB,OAAI,QAAQ,EAAG,QAAO;AACtB,UAAO,QAAQ,GAAG;;EAEpB,4BAAqC;AACnC,UAAO,kBAAkB;;EAE5B;;AAGH,SAAS,UAAU,MAAsB;AACvC,KAAI,OAAO,MAAM,KAAK,CAAE,QAAO;AAC/B,KAAI,OAAO,EAAG,QAAO;AACrB,KAAI,OAAO,EAAG,QAAO;AACrB,QAAO"}
1
+ {"version":3,"file":"sampling.js","names":["decisionMaker: SamplingDecisionMaker","windowStart: number | undefined"],"sources":["../../src/tracer/sampling.ts"],"sourcesContent":["/**\n * Head + tail sampling helpers. The tracer pairs the configured\n * sampler with a per-type override registry so chatty span types\n * (`memory.embed`, `tool.execute.partial`, …) can be downsampled\n * without affecting the rest of the trace stream.\n *\n * @packageDocumentation\n */\n\nimport type { SpanType } from '@graphorin/core';\n\n/** @stable */\nexport type SamplingDecisionMaker = 'parent-based' | 'always-on' | 'rate-limit';\n\n/**\n * Per-span-type rate override.\n *\n * @stable\n */\nexport interface SamplingRule {\n readonly type: SpanType | string;\n readonly rate: number;\n}\n\n/**\n * Configuration shape consumed by {@link createSampler}.\n *\n * @stable\n */\nexport interface SamplingOptions {\n /** Default head-sampling rate. Must be in `[0, 1]`. Defaults to `1.0`. */\n readonly rate?: number;\n /** Per-type overrides. Last write wins on duplicate `type`. */\n readonly rules?: ReadonlyArray<SamplingRule>;\n /** Decision maker. Defaults to `'parent-based'`. */\n readonly decisionMaker?: SamplingDecisionMaker;\n /**\n * Cap for the `'rate-limit'` decision maker: at most this many root spans\n * are sampled per rolling 1-second window (RP-19). `undefined` ⇒ no cap\n * (samples everything); `0` ⇒ sample nothing. Ignored by the other\n * decision makers.\n */\n readonly maxPerSecond?: number;\n /**\n * Clock for the `'rate-limit'` window. Defaults to `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n /**\n * Optional override for streaming-event sampling.\n * @see RB-52 - streaming event family `tool.execute.{progress,partial}`.\n */\n readonly streaming?: {\n readonly eventSamplingRate?: number;\n readonly includeChunkContent?: 'none' | 'text-only' | 'all';\n };\n /**\n * Override for the random source. Useful for deterministic tests.\n *\n * @internal\n */\n readonly random?: () => number;\n}\n\n/**\n * @stable\n */\nexport interface Sampler {\n /** Decide whether a span of the given type should be recorded. */\n shouldSample(type: SpanType | string, parentSampled?: boolean): boolean;\n /** Decide whether a span event of the given name should be recorded. */\n shouldRecordEvent(name: string): boolean;\n /** Returns whether chunk *content* should travel through the exporter. */\n shouldIncludeChunkContent(): boolean;\n}\n\n/**\n * Build a {@link Sampler} from the supplied options. The sampler is\n * intentionally inexpensive - every decision boils down to a single\n * `random < threshold` comparison.\n *\n * @stable\n */\nexport function createSampler(opts: SamplingOptions = {}): Sampler {\n const baseRate = clampRate(opts.rate ?? 1.0);\n const decisionMaker: SamplingDecisionMaker = opts.decisionMaker ?? 'parent-based';\n const ruleByType = new Map<string, number>();\n for (const rule of opts.rules ?? []) {\n ruleByType.set(rule.type, clampRate(rule.rate));\n }\n const streamingRate = clampRate(opts.streaming?.eventSamplingRate ?? 1.0);\n const includeChunks = opts.streaming?.includeChunkContent ?? 'none';\n const random = opts.random ?? Math.random;\n const maxPerSecond = opts.maxPerSecond;\n const now = opts.now ?? Date.now;\n let windowStart: number | undefined;\n let windowCount = 0;\n\n return {\n shouldSample(type, parentSampled): boolean {\n if (decisionMaker === 'always-on') return true;\n if (decisionMaker === 'parent-based') {\n // RP-19: true parent-based - follow the parent's real sampling\n // decision. A child of an unsampled parent is NOT recorded (it would\n // otherwise be an orphan); a root span (no parent) falls through to\n // the rate.\n if (parentSampled === true) return true;\n if (parentSampled === false) return false;\n }\n if (decisionMaker === 'rate-limit') {\n // RP-19: a real token-window limiter, distinct from the probabilistic\n // path. Caps sampled spans to `maxPerSecond` per rolling second.\n if (maxPerSecond === undefined) return true;\n if (maxPerSecond <= 0) return false;\n const t = now();\n if (windowStart === undefined || t - windowStart >= 1000) {\n windowStart = t;\n windowCount = 0;\n }\n if (windowCount < maxPerSecond) {\n windowCount += 1;\n return true;\n }\n return false;\n }\n const rate = ruleByType.get(type) ?? baseRate;\n if (rate >= 1) return true;\n if (rate <= 0) return false;\n return random() < rate;\n },\n shouldRecordEvent(name): boolean {\n const rate = ruleByType.get(name) ?? streamingRate;\n if (rate >= 1) return true;\n if (rate <= 0) return false;\n return random() < rate;\n },\n shouldIncludeChunkContent(): boolean {\n return includeChunks !== 'none';\n },\n };\n}\n\nfunction clampRate(rate: number): number {\n if (Number.isNaN(rate)) return 1;\n if (rate < 0) return 0;\n if (rate > 1) return 1;\n return rate;\n}\n"],"mappings":";;;;;;;;AAoFA,SAAgB,cAAc,OAAwB,EAAE,EAAW;CACjE,MAAM,WAAW,UAAU,KAAK,QAAQ,EAAI;CAC5C,MAAMA,gBAAuC,KAAK,iBAAiB;CACnE,MAAM,6BAAa,IAAI,KAAqB;AAC5C,MAAK,MAAM,QAAQ,KAAK,SAAS,EAAE,CACjC,YAAW,IAAI,KAAK,MAAM,UAAU,KAAK,KAAK,CAAC;CAEjD,MAAM,gBAAgB,UAAU,KAAK,WAAW,qBAAqB,EAAI;CACzE,MAAM,gBAAgB,KAAK,WAAW,uBAAuB;CAC7D,MAAM,SAAS,KAAK,UAAU,KAAK;CACnC,MAAM,eAAe,KAAK;CAC1B,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,IAAIC;CACJ,IAAI,cAAc;AAElB,QAAO;EACL,aAAa,MAAM,eAAwB;AACzC,OAAI,kBAAkB,YAAa,QAAO;AAC1C,OAAI,kBAAkB,gBAAgB;AAKpC,QAAI,kBAAkB,KAAM,QAAO;AACnC,QAAI,kBAAkB,MAAO,QAAO;;AAEtC,OAAI,kBAAkB,cAAc;AAGlC,QAAI,iBAAiB,OAAW,QAAO;AACvC,QAAI,gBAAgB,EAAG,QAAO;IAC9B,MAAM,IAAI,KAAK;AACf,QAAI,gBAAgB,UAAa,IAAI,eAAe,KAAM;AACxD,mBAAc;AACd,mBAAc;;AAEhB,QAAI,cAAc,cAAc;AAC9B,oBAAe;AACf,YAAO;;AAET,WAAO;;GAET,MAAM,OAAO,WAAW,IAAI,KAAK,IAAI;AACrC,OAAI,QAAQ,EAAG,QAAO;AACtB,OAAI,QAAQ,EAAG,QAAO;AACtB,UAAO,QAAQ,GAAG;;EAEpB,kBAAkB,MAAe;GAC/B,MAAM,OAAO,WAAW,IAAI,KAAK,IAAI;AACrC,OAAI,QAAQ,EAAG,QAAO;AACtB,OAAI,QAAQ,EAAG,QAAO;AACtB,UAAO,QAAQ,GAAG;;EAEpB,4BAAqC;AACnC,UAAO,kBAAkB;;EAE5B;;AAGH,SAAS,UAAU,MAAsB;AACvC,KAAI,OAAO,MAAM,KAAK,CAAE,QAAO;AAC/B,KAAI,OAAO,EAAG,QAAO;AACrB,KAAI,OAAO,EAAG,QAAO;AACrB,QAAO"}
@@ -5,7 +5,7 @@ import { SpanType } from "@graphorin/core";
5
5
  /**
6
6
  * @stable
7
7
  */
8
- declare function spanNameFor(type: SpanType): string;
8
+ declare function spanNameFor(type: SpanType, attrs?: Readonly<Record<string, unknown>>): string;
9
9
  //#endregion
10
10
  export { spanNameFor };
11
11
  //# sourceMappingURL=span-names.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"span-names.d.ts","names":[],"sources":["../../src/tracer/span-names.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAagB,WAAA,OAAkB"}
1
+ {"version":3,"file":"span-names.d.ts","names":[],"sources":["../../src/tracer/span-names.ts"],"sourcesContent":[],"mappings":";;;;;;;iBAgCgB,WAAA,OAAkB,kBAAkB,SAAS"}
@@ -1,9 +1,32 @@
1
1
  //#region src/tracer/span-names.ts
2
+ function attrString(attrs, keys) {
3
+ if (attrs === void 0) return void 0;
4
+ for (const key of keys) {
5
+ const value = attrs[key];
6
+ if (typeof value === "string" && value.length > 0) return value;
7
+ }
8
+ }
2
9
  /**
3
10
  * @stable
4
11
  */
5
- function spanNameFor(type) {
6
- return type;
12
+ function spanNameFor(type, attrs) {
13
+ switch (type) {
14
+ case "provider.generate":
15
+ case "provider.stream": {
16
+ const model = attrString(attrs, ["gen_ai.request.model", "graphorin.provider.model"]);
17
+ return model === void 0 ? type : `chat ${model}`;
18
+ }
19
+ case "tool.execute": {
20
+ const tool = attrString(attrs, ["gen_ai.tool.name", "graphorin.tool.name"]);
21
+ return tool === void 0 ? type : `execute_tool ${tool}`;
22
+ }
23
+ case "agent.run":
24
+ case "agent.step": {
25
+ const agent = attrString(attrs, ["gen_ai.agent.name", "gen_ai.agent.id"]);
26
+ return agent === void 0 ? type : `invoke_agent ${agent}`;
27
+ }
28
+ default: return type;
29
+ }
7
30
  }
8
31
 
9
32
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"span-names.js","names":[],"sources":["../../src/tracer/span-names.ts"],"sourcesContent":["/**\n * Default human-readable span name per {@link SpanType}. Operators\n * can override the name by passing `name` through a custom\n * `setAttributes(...)` call after `startSpan(...)` returns.\n *\n * @packageDocumentation\n */\n\nimport type { SpanType } from '@graphorin/core';\n\n/**\n * @stable\n */\nexport function spanNameFor(type: SpanType): string {\n return type;\n}\n"],"mappings":";;;;AAaA,SAAgB,YAAY,MAAwB;AAClD,QAAO"}
1
+ {"version":3,"file":"span-names.js","names":[],"sources":["../../src/tracer/span-names.ts"],"sourcesContent":["/**\n * Default human-readable span name per {@link SpanType}. Operators\n * can override the name by passing `name` through a custom\n * `setAttributes(...)` call after `startSpan(...)` returns.\n *\n * E8 (audit 2026-07-04): the OTel GenAI semantic conventions name spans\n * `\"{operation} {target}\"` (`chat gpt-4.1`, `execute_tool get_weather`,\n * `invoke_agent planner`) so trace UIs group like operations while\n * keeping the target visible. When the span's initial attributes carry\n * the target we emit that shape; otherwise the raw span type remains\n * the stable fallback.\n *\n * @packageDocumentation\n */\n\nimport type { SpanType } from '@graphorin/core';\n\nfunction attrString(\n attrs: Readonly<Record<string, unknown>> | undefined,\n keys: readonly string[],\n): string | undefined {\n if (attrs === undefined) return undefined;\n for (const key of keys) {\n const value = attrs[key];\n if (typeof value === 'string' && value.length > 0) return value;\n }\n return undefined;\n}\n\n/**\n * @stable\n */\nexport function spanNameFor(type: SpanType, attrs?: Readonly<Record<string, unknown>>): string {\n switch (type) {\n case 'provider.generate':\n case 'provider.stream': {\n const model = attrString(attrs, ['gen_ai.request.model', 'graphorin.provider.model']);\n return model === undefined ? type : `chat ${model}`;\n }\n case 'tool.execute': {\n const tool = attrString(attrs, ['gen_ai.tool.name', 'graphorin.tool.name']);\n return tool === undefined ? type : `execute_tool ${tool}`;\n }\n case 'agent.run':\n case 'agent.step': {\n const agent = attrString(attrs, ['gen_ai.agent.name', 'gen_ai.agent.id']);\n return agent === undefined ? type : `invoke_agent ${agent}`;\n }\n default:\n return type;\n }\n}\n"],"mappings":";AAiBA,SAAS,WACP,OACA,MACoB;AACpB,KAAI,UAAU,OAAW,QAAO;AAChC,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,MAAM;AACpB,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;;;;;;AAQ9D,SAAgB,YAAY,MAAgB,OAAmD;AAC7F,SAAQ,MAAR;EACE,KAAK;EACL,KAAK,mBAAmB;GACtB,MAAM,QAAQ,WAAW,OAAO,CAAC,wBAAwB,2BAA2B,CAAC;AACrF,UAAO,UAAU,SAAY,OAAO,QAAQ;;EAE9C,KAAK,gBAAgB;GACnB,MAAM,OAAO,WAAW,OAAO,CAAC,oBAAoB,sBAAsB,CAAC;AAC3E,UAAO,SAAS,SAAY,OAAO,gBAAgB;;EAErD,KAAK;EACL,KAAK,cAAc;GACjB,MAAM,QAAQ,WAAW,OAAO,CAAC,qBAAqB,kBAAkB,CAAC;AACzE,UAAO,UAAU,SAAY,OAAO,gBAAgB;;EAEtD,QACE,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"span.js","names":["attributes: Record<string, SpanAttributeValue>","sensitivities: Record<string, Sensitivity>","events: SpanRecord['events'][number][]","status: SpanStatus","statusMessage: string | undefined","attrs: SpanAttributes","record: SpanRecord<T>"],"sources":["../../src/tracer/span.ts"],"sourcesContent":["/**\n * Concrete `AISpan<T>` implementation. The span is intentionally\n * decoupled from `@opentelemetry/api`: the tracer materialises a\n * {@link SpanRecord} on `end()` and lets exporters do the rest.\n *\n * @packageDocumentation\n */\n\nimport type {\n AISpan,\n Sensitivity,\n SpanAttributes,\n SpanAttributeValue,\n SpanStatus,\n SpanType,\n} from '@graphorin/core';\n\nimport type { SpanRecord } from '../exporters/types.js';\n\n/**\n * Optional metadata accepted by `setAttribute(...)` to declare the\n * sensitivity of a single attribute. The validator uses the declared\n * tier when deciding whether to drop the attribute.\n *\n * @stable\n */\nexport interface SetAttributeOptions {\n readonly sensitivity?: Sensitivity;\n}\n\n/**\n * The internal span carries the convenience `setAttribute(...)` method\n * exposed by the tracer surface (per-attribute sensitivity tagging) on\n * top of the standard {@link AISpan} contract.\n *\n * @stable\n */\nexport interface GraphorinSpan<T extends SpanType = SpanType> extends AISpan<T> {\n setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void;\n}\n\n/**\n * @internal sink invoked when a span ends.\n */\nexport type SpanSink = (record: SpanRecord) => void;\n\n/**\n * @internal parameters passed by the tracer to {@link createSpan}.\n */\nexport interface SpanCreateInput<T extends SpanType = SpanType> {\n readonly type: T;\n readonly name: string;\n readonly traceId: string;\n readonly id: string;\n readonly parentId?: string;\n readonly attrs?: SpanAttributes;\n readonly attrSensitivities?: Readonly<Record<string, Sensitivity>>;\n readonly sink: SpanSink;\n readonly now: () => number;\n readonly recordEvent: (name: string) => boolean;\n}\n\n/**\n * @internal\n */\nexport function createSpan<T extends SpanType>(input: SpanCreateInput<T>): GraphorinSpan<T> {\n const startUnixNano = Math.floor(input.now() * 1_000_000);\n const attributes: Record<string, SpanAttributeValue> = { ...(input.attrs ?? {}) };\n const sensitivities: Record<string, Sensitivity> = { ...(input.attrSensitivities ?? {}) };\n const events: SpanRecord['events'][number][] = [];\n let status: SpanStatus = 'ok';\n let statusMessage: string | undefined;\n let ended = false;\n\n const span: GraphorinSpan<T> = {\n type: input.type,\n id: input.id,\n traceId: input.traceId,\n ...(input.parentId === undefined ? {} : { parentId: input.parentId }),\n setAttributes(attrs: SpanAttributes): void {\n if (ended) return;\n for (const [k, v] of Object.entries(attrs)) {\n attributes[k] = v;\n }\n },\n setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void {\n if (ended) return;\n attributes[name] = value;\n if (opts?.sensitivity !== undefined) {\n sensitivities[name] = opts.sensitivity;\n }\n },\n addEvent(name: string, attrs?: SpanAttributes): void {\n if (ended) return;\n if (!input.recordEvent(name)) return;\n events.push({\n name,\n timeUnixNano: Math.floor(input.now() * 1_000_000),\n attributes: Object.freeze({ ...(attrs ?? {}) }) as SpanAttributes,\n });\n },\n recordException(err: unknown): void {\n if (ended) return;\n const e = err as { name?: unknown; message?: unknown; stack?: unknown };\n const attrs: SpanAttributes = {\n 'exception.type': typeof e?.name === 'string' && e.name.length > 0 ? e.name : 'Error',\n 'exception.message': typeof e?.message === 'string' ? e.message : String(err),\n ...(typeof e?.stack === 'string' ? { 'exception.stacktrace': e.stack } : {}),\n };\n events.push({\n name: 'exception',\n timeUnixNano: Math.floor(input.now() * 1_000_000),\n attributes: attrs,\n });\n },\n setStatus(s: SpanStatus, message?: string): void {\n if (ended) return;\n status = s;\n if (message !== undefined) statusMessage = message;\n },\n end(): void {\n if (ended) return;\n ended = true;\n const endUnixNano = Math.floor(input.now() * 1_000_000);\n const record: SpanRecord<T> = {\n type: input.type,\n id: input.id,\n traceId: input.traceId,\n ...(input.parentId === undefined ? {} : { parentId: input.parentId }),\n name: input.name,\n startUnixNano,\n endUnixNano,\n status,\n ...(statusMessage === undefined ? {} : { statusMessage }),\n attributes: Object.freeze({ ...attributes }) as SpanAttributes,\n events: Object.freeze([...events]) as SpanRecord['events'],\n ...(Object.keys(sensitivities).length === 0\n ? {}\n : {\n sensitivityByAttribute: Object.freeze({ ...sensitivities }) as Readonly<\n Record<string, SpanAttributeValue>\n >,\n }),\n };\n input.sink(record);\n },\n };\n\n return span;\n}\n"],"mappings":";;;;AAiEA,SAAgB,WAA+B,OAA6C;CAC1F,MAAM,gBAAgB,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;CACzD,MAAMA,aAAiD,EAAE,GAAI,MAAM,SAAS,EAAE,EAAG;CACjF,MAAMC,gBAA6C,EAAE,GAAI,MAAM,qBAAqB,EAAE,EAAG;CACzF,MAAMC,SAAyC,EAAE;CACjD,IAAIC,SAAqB;CACzB,IAAIC;CACJ,IAAI,QAAQ;AA4EZ,QA1E+B;EAC7B,MAAM,MAAM;EACZ,IAAI,MAAM;EACV,SAAS,MAAM;EACf,GAAI,MAAM,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,MAAM,UAAU;EACpE,cAAc,OAA6B;AACzC,OAAI,MAAO;AACX,QAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,CACxC,YAAW,KAAK;;EAGpB,aAAa,MAAc,OAA2B,MAAkC;AACtF,OAAI,MAAO;AACX,cAAW,QAAQ;AACnB,OAAI,MAAM,gBAAgB,OACxB,eAAc,QAAQ,KAAK;;EAG/B,SAAS,MAAc,OAA8B;AACnD,OAAI,MAAO;AACX,OAAI,CAAC,MAAM,YAAY,KAAK,CAAE;AAC9B,UAAO,KAAK;IACV;IACA,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;IACjD,YAAY,OAAO,OAAO,EAAE,GAAI,SAAS,EAAE,EAAG,CAAC;IAChD,CAAC;;EAEJ,gBAAgB,KAAoB;AAClC,OAAI,MAAO;GACX,MAAM,IAAI;GACV,MAAMC,QAAwB;IAC5B,kBAAkB,OAAO,GAAG,SAAS,YAAY,EAAE,KAAK,SAAS,IAAI,EAAE,OAAO;IAC9E,qBAAqB,OAAO,GAAG,YAAY,WAAW,EAAE,UAAU,OAAO,IAAI;IAC7E,GAAI,OAAO,GAAG,UAAU,WAAW,EAAE,wBAAwB,EAAE,OAAO,GAAG,EAAE;IAC5E;AACD,UAAO,KAAK;IACV,MAAM;IACN,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;IACjD,YAAY;IACb,CAAC;;EAEJ,UAAU,GAAe,SAAwB;AAC/C,OAAI,MAAO;AACX,YAAS;AACT,OAAI,YAAY,OAAW,iBAAgB;;EAE7C,MAAY;AACV,OAAI,MAAO;AACX,WAAQ;GACR,MAAM,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;GACvD,MAAMC,SAAwB;IAC5B,MAAM,MAAM;IACZ,IAAI,MAAM;IACV,SAAS,MAAM;IACf,GAAI,MAAM,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,MAAM,UAAU;IACpE,MAAM,MAAM;IACZ;IACA;IACA;IACA,GAAI,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe;IACxD,YAAY,OAAO,OAAO,EAAE,GAAG,YAAY,CAAC;IAC5C,QAAQ,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC;IAClC,GAAI,OAAO,KAAK,cAAc,CAAC,WAAW,IACtC,EAAE,GACF,EACE,wBAAwB,OAAO,OAAO,EAAE,GAAG,eAAe,CAAC,EAG5D;IACN;AACD,SAAM,KAAK,OAAO;;EAErB"}
1
+ {"version":3,"file":"span.js","names":["attributes: Record<string, SpanAttributeValue>","sensitivities: Record<string, Sensitivity>","events: SpanRecord['events'][number][]","status: SpanStatus","statusMessage: string | undefined","attrs: SpanAttributes","record: SpanRecord<T>"],"sources":["../../src/tracer/span.ts"],"sourcesContent":["/**\n * Concrete `AISpan<T>` implementation. The span is intentionally\n * decoupled from `@opentelemetry/api`: the tracer materialises a\n * {@link SpanRecord} on `end()` and lets exporters do the rest.\n *\n * @packageDocumentation\n */\n\nimport type {\n AISpan,\n Sensitivity,\n SpanAttributes,\n SpanAttributeValue,\n SpanStatus,\n SpanType,\n} from '@graphorin/core';\n\nimport type { SpanRecord } from '../exporters/types.js';\n\n/**\n * Optional metadata accepted by `setAttribute(...)` to declare the\n * sensitivity of a single attribute. The validator uses the declared\n * tier when deciding whether to drop the attribute.\n *\n * @stable\n */\nexport interface SetAttributeOptions {\n readonly sensitivity?: Sensitivity;\n}\n\n/**\n * The internal span carries the convenience `setAttribute(...)` method\n * exposed by the tracer surface (per-attribute sensitivity tagging) on\n * top of the standard {@link AISpan} contract.\n *\n * @stable\n */\nexport interface GraphorinSpan<T extends SpanType = SpanType> extends AISpan<T> {\n setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void;\n}\n\n/**\n * @internal - sink invoked when a span ends.\n */\nexport type SpanSink = (record: SpanRecord) => void;\n\n/**\n * @internal - parameters passed by the tracer to {@link createSpan}.\n */\nexport interface SpanCreateInput<T extends SpanType = SpanType> {\n readonly type: T;\n readonly name: string;\n readonly traceId: string;\n readonly id: string;\n readonly parentId?: string;\n readonly attrs?: SpanAttributes;\n readonly attrSensitivities?: Readonly<Record<string, Sensitivity>>;\n readonly sink: SpanSink;\n readonly now: () => number;\n readonly recordEvent: (name: string) => boolean;\n}\n\n/**\n * @internal\n */\nexport function createSpan<T extends SpanType>(input: SpanCreateInput<T>): GraphorinSpan<T> {\n const startUnixNano = Math.floor(input.now() * 1_000_000);\n const attributes: Record<string, SpanAttributeValue> = { ...(input.attrs ?? {}) };\n const sensitivities: Record<string, Sensitivity> = { ...(input.attrSensitivities ?? {}) };\n const events: SpanRecord['events'][number][] = [];\n let status: SpanStatus = 'ok';\n let statusMessage: string | undefined;\n let ended = false;\n\n const span: GraphorinSpan<T> = {\n type: input.type,\n id: input.id,\n traceId: input.traceId,\n ...(input.parentId === undefined ? {} : { parentId: input.parentId }),\n setAttributes(attrs: SpanAttributes): void {\n if (ended) return;\n for (const [k, v] of Object.entries(attrs)) {\n attributes[k] = v;\n }\n },\n setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void {\n if (ended) return;\n attributes[name] = value;\n if (opts?.sensitivity !== undefined) {\n sensitivities[name] = opts.sensitivity;\n }\n },\n addEvent(name: string, attrs?: SpanAttributes): void {\n if (ended) return;\n if (!input.recordEvent(name)) return;\n events.push({\n name,\n timeUnixNano: Math.floor(input.now() * 1_000_000),\n attributes: Object.freeze({ ...(attrs ?? {}) }) as SpanAttributes,\n });\n },\n recordException(err: unknown): void {\n if (ended) return;\n const e = err as { name?: unknown; message?: unknown; stack?: unknown };\n const attrs: SpanAttributes = {\n 'exception.type': typeof e?.name === 'string' && e.name.length > 0 ? e.name : 'Error',\n 'exception.message': typeof e?.message === 'string' ? e.message : String(err),\n ...(typeof e?.stack === 'string' ? { 'exception.stacktrace': e.stack } : {}),\n };\n events.push({\n name: 'exception',\n timeUnixNano: Math.floor(input.now() * 1_000_000),\n attributes: attrs,\n });\n },\n setStatus(s: SpanStatus, message?: string): void {\n if (ended) return;\n status = s;\n if (message !== undefined) statusMessage = message;\n },\n end(): void {\n if (ended) return;\n ended = true;\n const endUnixNano = Math.floor(input.now() * 1_000_000);\n const record: SpanRecord<T> = {\n type: input.type,\n id: input.id,\n traceId: input.traceId,\n ...(input.parentId === undefined ? {} : { parentId: input.parentId }),\n name: input.name,\n startUnixNano,\n endUnixNano,\n status,\n ...(statusMessage === undefined ? {} : { statusMessage }),\n attributes: Object.freeze({ ...attributes }) as SpanAttributes,\n events: Object.freeze([...events]) as SpanRecord['events'],\n ...(Object.keys(sensitivities).length === 0\n ? {}\n : {\n sensitivityByAttribute: Object.freeze({ ...sensitivities }) as Readonly<\n Record<string, SpanAttributeValue>\n >,\n }),\n };\n input.sink(record);\n },\n };\n\n return span;\n}\n"],"mappings":";;;;AAiEA,SAAgB,WAA+B,OAA6C;CAC1F,MAAM,gBAAgB,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;CACzD,MAAMA,aAAiD,EAAE,GAAI,MAAM,SAAS,EAAE,EAAG;CACjF,MAAMC,gBAA6C,EAAE,GAAI,MAAM,qBAAqB,EAAE,EAAG;CACzF,MAAMC,SAAyC,EAAE;CACjD,IAAIC,SAAqB;CACzB,IAAIC;CACJ,IAAI,QAAQ;AA4EZ,QA1E+B;EAC7B,MAAM,MAAM;EACZ,IAAI,MAAM;EACV,SAAS,MAAM;EACf,GAAI,MAAM,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,MAAM,UAAU;EACpE,cAAc,OAA6B;AACzC,OAAI,MAAO;AACX,QAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,CACxC,YAAW,KAAK;;EAGpB,aAAa,MAAc,OAA2B,MAAkC;AACtF,OAAI,MAAO;AACX,cAAW,QAAQ;AACnB,OAAI,MAAM,gBAAgB,OACxB,eAAc,QAAQ,KAAK;;EAG/B,SAAS,MAAc,OAA8B;AACnD,OAAI,MAAO;AACX,OAAI,CAAC,MAAM,YAAY,KAAK,CAAE;AAC9B,UAAO,KAAK;IACV;IACA,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;IACjD,YAAY,OAAO,OAAO,EAAE,GAAI,SAAS,EAAE,EAAG,CAAC;IAChD,CAAC;;EAEJ,gBAAgB,KAAoB;AAClC,OAAI,MAAO;GACX,MAAM,IAAI;GACV,MAAMC,QAAwB;IAC5B,kBAAkB,OAAO,GAAG,SAAS,YAAY,EAAE,KAAK,SAAS,IAAI,EAAE,OAAO;IAC9E,qBAAqB,OAAO,GAAG,YAAY,WAAW,EAAE,UAAU,OAAO,IAAI;IAC7E,GAAI,OAAO,GAAG,UAAU,WAAW,EAAE,wBAAwB,EAAE,OAAO,GAAG,EAAE;IAC5E;AACD,UAAO,KAAK;IACV,MAAM;IACN,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;IACjD,YAAY;IACb,CAAC;;EAEJ,UAAU,GAAe,SAAwB;AAC/C,OAAI,MAAO;AACX,YAAS;AACT,OAAI,YAAY,OAAW,iBAAgB;;EAE7C,MAAY;AACV,OAAI,MAAO;AACX,WAAQ;GACR,MAAM,cAAc,KAAK,MAAM,MAAM,KAAK,GAAG,IAAU;GACvD,MAAMC,SAAwB;IAC5B,MAAM,MAAM;IACZ,IAAI,MAAM;IACV,SAAS,MAAM;IACf,GAAI,MAAM,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,MAAM,UAAU;IACpE,MAAM,MAAM;IACZ;IACA;IACA;IACA,GAAI,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe;IACxD,YAAY,OAAO,OAAO,EAAE,GAAG,YAAY,CAAC;IAC5C,QAAQ,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC;IAClC,GAAI,OAAO,KAAK,cAAc,CAAC,WAAW,IACtC,EAAE,GACF,EACE,wBAAwB,OAAO,OAAO,EAAE,GAAG,eAAe,CAAC,EAG5D;IACN;AACD,SAAM,KAAK,OAAO;;EAErB"}
@@ -16,7 +16,7 @@ interface TracerOptions {
16
16
  readonly serviceName?: string;
17
17
  /**
18
18
  * Configured exporters. Each exporter MUST be wrapped via
19
- * `withValidation(...)` before reaching the tracer pass them
19
+ * `withValidation(...)` before reaching the tracer - pass them
20
20
  * through unwrapped to use the tracer-managed validator (set by
21
21
  * `validation`) or pass already-wrapped exporters when you want
22
22
  * per-exporter policies.
@@ -32,7 +32,7 @@ function createTracer(opts) {
32
32
  let validator = null;
33
33
  let exporters = [];
34
34
  if (validationMode === "off") {
35
- warnSink("[graphorin/observability] WARN: validation: 'off' exporters are NOT auto-wrapped. Every exporter must call withValidation(...) explicitly. See ADR-035: OTLP RedactionValidator + default-deny non-public.");
35
+ warnSink("[graphorin/observability] WARN: validation: 'off' - exporters are NOT auto-wrapped. Every exporter must call withValidation(...) explicitly. See ADR-035: OTLP RedactionValidator + default-deny non-public.");
36
36
  for (const exporter of opts.exporters) {
37
37
  if (!isValidatedExporter(exporter)) throw new UnvalidatedExporterError(exporter.id);
38
38
  exporters.push(exporter);
@@ -67,7 +67,7 @@ function createTracer(opts) {
67
67
  const sensitivities = readAttrSensitivities(spec.attrs);
68
68
  return createSpan({
69
69
  type: spec.type,
70
- name: spanNameFor(spec.type),
70
+ name: spanNameFor(spec.type, spec.attrs),
71
71
  traceId,
72
72
  id,
73
73
  ...parentId === void 0 ? {} : { parentId },
@@ -1 +1 @@
1
- {"version":3,"file":"tracer.js","names":["DEFAULT_VALIDATION: RedactionValidatorOptions","defaultAttrSensitivity: Sensitivity","validator: RedactionValidatorInstance | null","exporters: TraceExporter[]","out: Record<string, Sensitivity>"],"sources":["../../src/tracer/tracer.ts"],"sourcesContent":["/**\n * `createTracer(...)` — the public entry point for the observability\n * tracer. Wires together the {@link Sampler}, the\n * {@link RedactionValidator}, and the registered {@link TraceExporter}s.\n *\n * @packageDocumentation\n */\n\nimport type {\n AISpan,\n Sensitivity,\n SpanAttributeValue,\n SpanType,\n StartSpanOptions,\n Tracer,\n} from '@graphorin/core';\n\nimport { createConsoleExporter } from '../exporters/console.js';\nimport type { SpanRecord, TraceExporter } from '../exporters/types.js';\nimport { isValidatedExporter, withValidation } from '../exporters/with-validation.js';\nimport { UnvalidatedExporterError } from '../redaction/errors.js';\nimport type { RedactionValidatorInstance, RedactionValidatorOptions } from '../redaction/types.js';\nimport { createRedactionValidator } from '../redaction/validator.js';\n\nimport { newSpanId, newTraceId } from './ids.js';\nimport { createSampler, type SamplingOptions } from './sampling.js';\nimport { createSpan, type GraphorinSpan } from './span.js';\nimport { spanNameFor } from './span-names.js';\n\n/**\n * Configuration shape consumed by {@link createTracer}.\n *\n * @stable\n */\nexport interface TracerOptions {\n /** Logical service name. Embedded in the OTLP `Resource`. */\n readonly serviceName?: string;\n /**\n * Configured exporters. Each exporter MUST be wrapped via\n * `withValidation(...)` before reaching the tracer — pass them\n * through unwrapped to use the tracer-managed validator (set by\n * `validation`) or pass already-wrapped exporters when you want\n * per-exporter policies.\n */\n readonly exporters: ReadonlyArray<TraceExporter>;\n /**\n * Tracer-managed validator policy.\n *\n * - `RedactionValidatorOptions` (default): the tracer auto-wraps any\n * un-wrapped exporter with `withValidation(...)` using these\n * options.\n * - `'off'`: NOT recommended. Skips auto-wrap, and the tracer logs a\n * startup WARN to the supplied {@link warnSink}. Pre-wrapped\n * exporters still flow through their validators.\n *\n * @default `{ minTier: 'public', failOnUnredactedSensitive: false }`\n */\n readonly validation?: RedactionValidatorOptions | 'off';\n /** Sampler configuration. */\n readonly sampling?: SamplingOptions;\n /**\n * Default sensitivity for attributes that omit `setAttribute(_, _, { sensitivity })`.\n * Defaults to `'internal'` per the default-deny non-public posture.\n */\n readonly defaultAttributeSensitivity?: Sensitivity;\n /**\n * Sink used for startup WARNings. Defaults to `console.warn`.\n *\n * @internal\n */\n readonly warnSink?: (line: string) => void;\n /**\n * Override the wall clock used for span timestamps. Returns\n * milliseconds-since-epoch as a `number`. Default: `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/**\n * The {@link createTracer} return value extends the standard\n * {@link Tracer} contract from `@graphorin/core` with introspection\n * helpers (counter snapshots, validator handle).\n *\n * @stable\n */\nexport interface GraphorinTracer extends Tracer {\n /** Service name embedded in the OTLP resource. */\n readonly serviceName: string;\n /**\n * Snapshot of the redaction counters (`droppedTotal`,\n * `droppedByReason`, `matchesByPattern`) maintained by the\n * tracer-managed validator.\n */\n getMetrics(): import('../redaction/types.js').RedactionCounters;\n /** The tracer-managed validator. `null` when `validation: 'off'`. */\n readonly validator: RedactionValidatorInstance | null;\n /** Force-flush every registered exporter. */\n flush(): Promise<void>;\n}\n\nconst DEFAULT_VALIDATION: RedactionValidatorOptions = Object.freeze({\n minTier: 'public',\n failOnUnredactedSensitive: false,\n});\n\n/**\n * Build a {@link GraphorinTracer} from the supplied options. Every\n * exporter passed in must already be wrapped via\n * `withValidation(...)` OR `validation` must be set to a concrete\n * options object so the tracer can auto-wrap on your behalf.\n * Registering a raw exporter while `validation: 'off'` triggers an\n * {@link UnvalidatedExporterError} at startup.\n *\n * @stable\n */\nexport function createTracer(opts: TracerOptions): GraphorinTracer {\n const serviceName = opts.serviceName ?? 'graphorin';\n const sampler = createSampler(opts.sampling ?? {});\n const warnSink = opts.warnSink ?? ((line: string) => console.warn(line));\n const now = opts.now ?? (() => Date.now());\n const defaultAttrSensitivity: Sensitivity = opts.defaultAttributeSensitivity ?? 'internal';\n\n const validationMode = opts.validation ?? DEFAULT_VALIDATION;\n\n let validator: RedactionValidatorInstance | null = null;\n let exporters: TraceExporter[] = [];\n\n if (validationMode === 'off') {\n warnSink(\n \"[graphorin/observability] WARN: validation: 'off' — exporters are NOT \" +\n 'auto-wrapped. Every exporter must call withValidation(...) explicitly. ' +\n 'See ADR-035: OTLP RedactionValidator + default-deny non-public.',\n );\n for (const exporter of opts.exporters) {\n if (!isValidatedExporter(exporter)) {\n throw new UnvalidatedExporterError(exporter.id);\n }\n exporters.push(exporter);\n }\n } else {\n validator = createRedactionValidator(validationMode);\n for (const exporter of opts.exporters) {\n exporters.push(\n isValidatedExporter(exporter) ? exporter : withValidation(exporter, { validator }),\n );\n }\n }\n\n if (exporters.length === 0) {\n // The tracer is still useful (it emits records to /dev/null) but\n // we want to surface that explicitly so consumers don't think the\n // logger is broken.\n const fallback = withValidation(createConsoleExporter({ id: 'console-fallback' }), {\n validator: validator ?? createRedactionValidator(DEFAULT_VALIDATION),\n });\n warnSink(\n '[graphorin/observability] WARN: createTracer() received zero exporters. ' +\n 'Falling back to a sanitized ConsoleExporter so spans are not lost.',\n );\n exporters = [fallback];\n }\n\n // RP-20: sink() exports are fire-and-forget; hold each promise so flush()\n // can await it. Without this a span emitted moments before shutdown is lost\n // when the exporter's closed-guard trips before its export() resolves.\n const inFlight = new Set<Promise<void>>();\n\n function sink(record: SpanRecord): void {\n for (const exporter of exporters) {\n const p = Promise.resolve()\n .then(() => exporter.export(record))\n .catch((err: unknown) => {\n warnSink(\n `[graphorin/observability] WARN: exporter ${exporter.id} failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n });\n inFlight.add(p);\n void p.finally(() => {\n inFlight.delete(p);\n });\n }\n }\n\n function startSpan<T extends SpanType>(spec: StartSpanOptions<T>): AISpan<T> {\n const traceId = spec.parent?.traceId ?? newTraceId();\n const id = newSpanId();\n const parentId = spec.parent?.id;\n // RP-19: a real parent-sampled flag for `'parent-based'` sampling. An\n // unsampled parent is a noop span (no `setAttribute`), so `asGraphorinSpan`\n // returns null — `parentId !== undefined` used to treat that as\n // parent-sampled and record the child as an orphan.\n const parentSampled =\n spec.parent === undefined ? undefined : asGraphorinSpan(spec.parent) !== null;\n const sampled = sampler.shouldSample(spec.type, parentSampled);\n if (!sampled) {\n return noopSpan<T>(spec.type, traceId, id, parentId);\n }\n const sensitivities = readAttrSensitivities(spec.attrs);\n return createSpan<T>({\n type: spec.type,\n name: spanNameFor(spec.type),\n traceId,\n id,\n ...(parentId === undefined ? {} : { parentId }),\n ...(spec.attrs === undefined ? {} : { attrs: spec.attrs }),\n ...(sensitivities === undefined ? {} : { attrSensitivities: sensitivities }),\n sink,\n now,\n recordEvent: (name) => sampler.shouldRecordEvent(name),\n }) as unknown as AISpan<T>;\n }\n\n async function span<T extends SpanType, R>(\n spec: StartSpanOptions<T>,\n fn: (s: AISpan<T>) => R | Promise<R>,\n ): Promise<R> {\n const s = startSpan(spec);\n try {\n const out = await fn(s);\n s.setStatus('ok');\n return out;\n } catch (err) {\n s.recordException(err);\n s.setStatus('error', err instanceof Error ? err.message : String(err));\n throw err;\n } finally {\n s.end();\n }\n }\n\n async function flush(): Promise<void> {\n // RP-20: drain in-flight fire-and-forget exports first, then flush.\n while (inFlight.size > 0) {\n await Promise.all([...inFlight]);\n }\n await Promise.all(exporters.map((e) => e.flush()));\n }\n\n async function shutdown(): Promise<void> {\n await flush();\n await Promise.all(exporters.map((e) => e.shutdown()));\n }\n\n return {\n startSpan,\n span,\n flush,\n shutdown,\n serviceName,\n getMetrics: () =>\n validator?.counters() ?? {\n droppedTotal: 0,\n droppedByReason: Object.freeze({}),\n matchesByPattern: Object.freeze({}),\n },\n validator,\n };\n\n function readAttrSensitivities(\n attrs: undefined | Readonly<Record<string, SpanAttributeValue>>,\n ): Readonly<Record<string, Sensitivity>> | undefined {\n if (attrs === undefined) return undefined;\n // RP-19: initial attrs that omit an explicit `setAttribute(_, _, {\n // sensitivity })` default to `defaultAttributeSensitivity`. Threading it\n // here makes the knob effective — untagged framework attributes carry the\n // configured tier instead of the validator's hardcoded fallback.\n const out: Record<string, Sensitivity> = {};\n for (const key of Object.keys(attrs)) out[key] = defaultAttrSensitivity;\n return Object.freeze(out);\n }\n\n function noopSpan<T extends SpanType>(\n type: T,\n traceId: string,\n id: string,\n parentId?: string,\n ): AISpan<T> {\n const stub: AISpan<T> = {\n type,\n id,\n traceId,\n ...(parentId === undefined ? {} : { parentId }),\n setAttributes() {},\n addEvent() {},\n recordException() {},\n setStatus() {},\n end() {},\n };\n return stub;\n }\n}\n\n/**\n * Returns the underlying {@link GraphorinSpan} when `span` is a Graphorin\n * span. Useful when callers want to reach the per-attribute sensitivity\n * helper from a generic `AISpan<T>`.\n *\n * @stable\n */\nexport function asGraphorinSpan<T extends SpanType>(span: AISpan<T>): GraphorinSpan<T> | null {\n if (typeof (span as Partial<GraphorinSpan<T>>).setAttribute === 'function') {\n return span as GraphorinSpan<T>;\n }\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tracer.js","names":["DEFAULT_VALIDATION: RedactionValidatorOptions","defaultAttrSensitivity: Sensitivity","validator: RedactionValidatorInstance | null","exporters: TraceExporter[]","out: Record<string, Sensitivity>"],"sources":["../../src/tracer/tracer.ts"],"sourcesContent":["/**\n * `createTracer(...)` - the public entry point for the observability\n * tracer. Wires together the {@link Sampler}, the\n * {@link RedactionValidator}, and the registered {@link TraceExporter}s.\n *\n * @packageDocumentation\n */\n\nimport type {\n AISpan,\n Sensitivity,\n SpanAttributeValue,\n SpanType,\n StartSpanOptions,\n Tracer,\n} from '@graphorin/core';\n\nimport { createConsoleExporter } from '../exporters/console.js';\nimport type { SpanRecord, TraceExporter } from '../exporters/types.js';\nimport { isValidatedExporter, withValidation } from '../exporters/with-validation.js';\nimport { UnvalidatedExporterError } from '../redaction/errors.js';\nimport type { RedactionValidatorInstance, RedactionValidatorOptions } from '../redaction/types.js';\nimport { createRedactionValidator } from '../redaction/validator.js';\n\nimport { newSpanId, newTraceId } from './ids.js';\nimport { createSampler, type SamplingOptions } from './sampling.js';\nimport { createSpan, type GraphorinSpan } from './span.js';\nimport { spanNameFor } from './span-names.js';\n\n/**\n * Configuration shape consumed by {@link createTracer}.\n *\n * @stable\n */\nexport interface TracerOptions {\n /** Logical service name. Embedded in the OTLP `Resource`. */\n readonly serviceName?: string;\n /**\n * Configured exporters. Each exporter MUST be wrapped via\n * `withValidation(...)` before reaching the tracer - pass them\n * through unwrapped to use the tracer-managed validator (set by\n * `validation`) or pass already-wrapped exporters when you want\n * per-exporter policies.\n */\n readonly exporters: ReadonlyArray<TraceExporter>;\n /**\n * Tracer-managed validator policy.\n *\n * - `RedactionValidatorOptions` (default): the tracer auto-wraps any\n * un-wrapped exporter with `withValidation(...)` using these\n * options.\n * - `'off'`: NOT recommended. Skips auto-wrap, and the tracer logs a\n * startup WARN to the supplied {@link warnSink}. Pre-wrapped\n * exporters still flow through their validators.\n *\n * @default `{ minTier: 'public', failOnUnredactedSensitive: false }`\n */\n readonly validation?: RedactionValidatorOptions | 'off';\n /** Sampler configuration. */\n readonly sampling?: SamplingOptions;\n /**\n * Default sensitivity for attributes that omit `setAttribute(_, _, { sensitivity })`.\n * Defaults to `'internal'` per the default-deny non-public posture.\n */\n readonly defaultAttributeSensitivity?: Sensitivity;\n /**\n * Sink used for startup WARNings. Defaults to `console.warn`.\n *\n * @internal\n */\n readonly warnSink?: (line: string) => void;\n /**\n * Override the wall clock used for span timestamps. Returns\n * milliseconds-since-epoch as a `number`. Default: `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/**\n * The {@link createTracer} return value extends the standard\n * {@link Tracer} contract from `@graphorin/core` with introspection\n * helpers (counter snapshots, validator handle).\n *\n * @stable\n */\nexport interface GraphorinTracer extends Tracer {\n /** Service name embedded in the OTLP resource. */\n readonly serviceName: string;\n /**\n * Snapshot of the redaction counters (`droppedTotal`,\n * `droppedByReason`, `matchesByPattern`) maintained by the\n * tracer-managed validator.\n */\n getMetrics(): import('../redaction/types.js').RedactionCounters;\n /** The tracer-managed validator. `null` when `validation: 'off'`. */\n readonly validator: RedactionValidatorInstance | null;\n /** Force-flush every registered exporter. */\n flush(): Promise<void>;\n}\n\nconst DEFAULT_VALIDATION: RedactionValidatorOptions = Object.freeze({\n minTier: 'public',\n failOnUnredactedSensitive: false,\n});\n\n/**\n * Build a {@link GraphorinTracer} from the supplied options. Every\n * exporter passed in must already be wrapped via\n * `withValidation(...)` OR `validation` must be set to a concrete\n * options object so the tracer can auto-wrap on your behalf.\n * Registering a raw exporter while `validation: 'off'` triggers an\n * {@link UnvalidatedExporterError} at startup.\n *\n * @stable\n */\nexport function createTracer(opts: TracerOptions): GraphorinTracer {\n const serviceName = opts.serviceName ?? 'graphorin';\n const sampler = createSampler(opts.sampling ?? {});\n const warnSink = opts.warnSink ?? ((line: string) => console.warn(line));\n const now = opts.now ?? (() => Date.now());\n const defaultAttrSensitivity: Sensitivity = opts.defaultAttributeSensitivity ?? 'internal';\n\n const validationMode = opts.validation ?? DEFAULT_VALIDATION;\n\n let validator: RedactionValidatorInstance | null = null;\n let exporters: TraceExporter[] = [];\n\n if (validationMode === 'off') {\n warnSink(\n \"[graphorin/observability] WARN: validation: 'off' - exporters are NOT \" +\n 'auto-wrapped. Every exporter must call withValidation(...) explicitly. ' +\n 'See ADR-035: OTLP RedactionValidator + default-deny non-public.',\n );\n for (const exporter of opts.exporters) {\n if (!isValidatedExporter(exporter)) {\n throw new UnvalidatedExporterError(exporter.id);\n }\n exporters.push(exporter);\n }\n } else {\n validator = createRedactionValidator(validationMode);\n for (const exporter of opts.exporters) {\n exporters.push(\n isValidatedExporter(exporter) ? exporter : withValidation(exporter, { validator }),\n );\n }\n }\n\n if (exporters.length === 0) {\n // The tracer is still useful (it emits records to /dev/null) but\n // we want to surface that explicitly so consumers don't think the\n // logger is broken.\n const fallback = withValidation(createConsoleExporter({ id: 'console-fallback' }), {\n validator: validator ?? createRedactionValidator(DEFAULT_VALIDATION),\n });\n warnSink(\n '[graphorin/observability] WARN: createTracer() received zero exporters. ' +\n 'Falling back to a sanitized ConsoleExporter so spans are not lost.',\n );\n exporters = [fallback];\n }\n\n // RP-20: sink() exports are fire-and-forget; hold each promise so flush()\n // can await it. Without this a span emitted moments before shutdown is lost\n // when the exporter's closed-guard trips before its export() resolves.\n const inFlight = new Set<Promise<void>>();\n\n function sink(record: SpanRecord): void {\n for (const exporter of exporters) {\n const p = Promise.resolve()\n .then(() => exporter.export(record))\n .catch((err: unknown) => {\n warnSink(\n `[graphorin/observability] WARN: exporter ${exporter.id} failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n });\n inFlight.add(p);\n void p.finally(() => {\n inFlight.delete(p);\n });\n }\n }\n\n function startSpan<T extends SpanType>(spec: StartSpanOptions<T>): AISpan<T> {\n const traceId = spec.parent?.traceId ?? newTraceId();\n const id = newSpanId();\n const parentId = spec.parent?.id;\n // RP-19: a real parent-sampled flag for `'parent-based'` sampling. An\n // unsampled parent is a noop span (no `setAttribute`), so `asGraphorinSpan`\n // returns null - `parentId !== undefined` used to treat that as\n // parent-sampled and record the child as an orphan.\n const parentSampled =\n spec.parent === undefined ? undefined : asGraphorinSpan(spec.parent) !== null;\n const sampled = sampler.shouldSample(spec.type, parentSampled);\n if (!sampled) {\n return noopSpan<T>(spec.type, traceId, id, parentId);\n }\n const sensitivities = readAttrSensitivities(spec.attrs);\n return createSpan<T>({\n type: spec.type,\n name: spanNameFor(spec.type, spec.attrs),\n traceId,\n id,\n ...(parentId === undefined ? {} : { parentId }),\n ...(spec.attrs === undefined ? {} : { attrs: spec.attrs }),\n ...(sensitivities === undefined ? {} : { attrSensitivities: sensitivities }),\n sink,\n now,\n recordEvent: (name) => sampler.shouldRecordEvent(name),\n }) as unknown as AISpan<T>;\n }\n\n async function span<T extends SpanType, R>(\n spec: StartSpanOptions<T>,\n fn: (s: AISpan<T>) => R | Promise<R>,\n ): Promise<R> {\n const s = startSpan(spec);\n try {\n const out = await fn(s);\n s.setStatus('ok');\n return out;\n } catch (err) {\n s.recordException(err);\n s.setStatus('error', err instanceof Error ? err.message : String(err));\n throw err;\n } finally {\n s.end();\n }\n }\n\n async function flush(): Promise<void> {\n // RP-20: drain in-flight fire-and-forget exports first, then flush.\n while (inFlight.size > 0) {\n await Promise.all([...inFlight]);\n }\n await Promise.all(exporters.map((e) => e.flush()));\n }\n\n async function shutdown(): Promise<void> {\n await flush();\n await Promise.all(exporters.map((e) => e.shutdown()));\n }\n\n return {\n startSpan,\n span,\n flush,\n shutdown,\n serviceName,\n getMetrics: () =>\n validator?.counters() ?? {\n droppedTotal: 0,\n droppedByReason: Object.freeze({}),\n matchesByPattern: Object.freeze({}),\n },\n validator,\n };\n\n function readAttrSensitivities(\n attrs: undefined | Readonly<Record<string, SpanAttributeValue>>,\n ): Readonly<Record<string, Sensitivity>> | undefined {\n if (attrs === undefined) return undefined;\n // RP-19: initial attrs that omit an explicit `setAttribute(_, _, {\n // sensitivity })` default to `defaultAttributeSensitivity`. Threading it\n // here makes the knob effective - untagged framework attributes carry the\n // configured tier instead of the validator's hardcoded fallback.\n const out: Record<string, Sensitivity> = {};\n for (const key of Object.keys(attrs)) out[key] = defaultAttrSensitivity;\n return Object.freeze(out);\n }\n\n function noopSpan<T extends SpanType>(\n type: T,\n traceId: string,\n id: string,\n parentId?: string,\n ): AISpan<T> {\n const stub: AISpan<T> = {\n type,\n id,\n traceId,\n ...(parentId === undefined ? {} : { parentId }),\n setAttributes() {},\n addEvent() {},\n recordException() {},\n setStatus() {},\n end() {},\n };\n return stub;\n }\n}\n\n/**\n * Returns the underlying {@link GraphorinSpan} when `span` is a Graphorin\n * span. Useful when callers want to reach the per-attribute sensitivity\n * helper from a generic `AISpan<T>`.\n *\n * @stable\n */\nexport function asGraphorinSpan<T extends SpanType>(span: AISpan<T>): GraphorinSpan<T> | null {\n if (typeof (span as Partial<GraphorinSpan<T>>).setAttribute === 'function') {\n return span as GraphorinSpan<T>;\n }\n return null;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/observability",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Observability primitives for the Graphorin framework: typed AISpan tracer over OpenTelemetry, mandatory withValidation wrapper for every exporter, sensitivity-aware RedactionValidator with built-in PII / secret patterns, GenAI Semantic Conventions conformance helpers, OpenInference span-kind emission, structured logger with span correlation, append-only JSONL exporter for replay, sanitized replay primitives, hierarchical CostTracker, and a minimal inline eval runner alongside a zero-default telemetry stub.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -100,7 +100,7 @@
100
100
  "LICENSE"
101
101
  ],
102
102
  "dependencies": {
103
- "@graphorin/core": "0.5.0"
103
+ "@graphorin/core": "0.6.1"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@opentelemetry/api": "^1.9.0",