@modainteract/moda-interact-shared 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,6 +127,41 @@ await observeConversationTurn("whatsapp", () =>
127
127
  );
128
128
  ```
129
129
 
130
+ Pass `recordMetrics: false` to activate a helper's span without recording its
131
+ GenAI metrics. The option applies only to that helper invocation, so pass it to
132
+ each nested helper whose metrics should be suppressed:
133
+
134
+ ```ts
135
+ const spansOnly = { recordMetrics: false } as const;
136
+
137
+ await observeConversationTurn("whatsapp", () =>
138
+ observeAgentInvocation({ agentName: "commerce-agent" }, () =>
139
+ observeAgentTool("lookup-products", executeTool, spansOnly),
140
+ spansOnly,
141
+ ),
142
+ spansOnly,
143
+ );
144
+ ```
145
+
146
+ An optional `mapException` callback can replace the application failure with a
147
+ bounded telemetry-safe representation for `span.recordException`. The original
148
+ thrown value is still rethrown unchanged. If the mapper throws or returns no
149
+ usable fields, the exception event is omitted and the span remains `ERROR`;
150
+ the original value is never recorded as fallback. Apply the mapper to every
151
+ nested helper that may observe the same rethrown failure:
152
+
153
+ ```ts
154
+ const safeObservation = {
155
+ recordMetrics: false,
156
+ mapException: () => ({
157
+ name: "ProviderError",
158
+ message: "Provider operation failed",
159
+ }),
160
+ } as const;
161
+
162
+ await observeAgentTool("lookup-products", executeTool, safeObservation);
163
+ ```
164
+
130
165
  The helpers use only the global tracer and meter providers installed by the
131
166
  Node runtime. They create no SDK, provider, exporter, or network request, and
132
167
  remain lightweight no-ops when providers are absent. Six module-singleton
@@ -0,0 +1,73 @@
1
+ // src/observability/index.ts
2
+ import {
3
+ context,
4
+ SpanStatusCode,
5
+ trace
6
+ } from "@opentelemetry/api";
7
+ var TRACER_NAME = "@modainteract/moda-interact-shared/observability";
8
+ var MAX_MAPPED_EXCEPTION_FIELD_LENGTH = 1024;
9
+ function getActiveTraceId() {
10
+ return trace.getSpan(context.active())?.spanContext().traceId;
11
+ }
12
+ async function withObservedSpan(name, attributes, work, options = {}) {
13
+ const tracer = trace.getTracer(TRACER_NAME);
14
+ return tracer.startActiveSpan(name, { attributes }, async (span) => {
15
+ try {
16
+ const result = await work();
17
+ span.setStatus({ code: SpanStatusCode.OK });
18
+ return result;
19
+ } catch (error) {
20
+ if (options.mapException) {
21
+ try {
22
+ const mappedException = normalizeMappedException(
23
+ options.mapException(error)
24
+ );
25
+ if (mappedException !== void 0) {
26
+ span.recordException(mappedException);
27
+ }
28
+ } catch {
29
+ }
30
+ } else {
31
+ span.recordException(
32
+ error instanceof Error ? error : new Error(String(error))
33
+ );
34
+ }
35
+ span.setStatus({ code: SpanStatusCode.ERROR });
36
+ throw error;
37
+ } finally {
38
+ span.end();
39
+ }
40
+ });
41
+ }
42
+ function normalizeMappedException(exception) {
43
+ if (typeof exception === "string") {
44
+ return boundedExceptionField(exception);
45
+ }
46
+ if (!exception || typeof exception !== "object") {
47
+ return void 0;
48
+ }
49
+ const code = typeof exception.code === "number" ? exception.code : boundedExceptionField(exception.code);
50
+ const message = boundedExceptionField(exception.message);
51
+ const name = boundedExceptionField(exception.name);
52
+ const stack = boundedExceptionField(exception.stack);
53
+ if (code === void 0 && message === void 0 && name === void 0) {
54
+ return void 0;
55
+ }
56
+ return {
57
+ ...code !== void 0 ? { code } : {},
58
+ ...message !== void 0 ? { message } : {},
59
+ ...name !== void 0 ? { name } : {},
60
+ ...stack !== void 0 ? { stack } : {}
61
+ };
62
+ }
63
+ function boundedExceptionField(value) {
64
+ if (typeof value !== "string") return void 0;
65
+ const bounded = value.trim().slice(0, MAX_MAPPED_EXCEPTION_FIELD_LENGTH);
66
+ return bounded || void 0;
67
+ }
68
+
69
+ export {
70
+ getActiveTraceId,
71
+ withObservedSpan
72
+ };
73
+ //# sourceMappingURL=chunk-A3EK6N46.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/observability/index.ts"],"sourcesContent":["import {\n context,\n SpanStatusCode,\n trace,\n type Attributes,\n type Exception,\n} from \"@opentelemetry/api\";\n\nconst TRACER_NAME = \"@modainteract/moda-interact-shared/observability\";\nconst MAX_MAPPED_EXCEPTION_FIELD_LENGTH = 1_024;\n\nexport type SpanExceptionMapper = (error: unknown) => Exception | undefined;\n\nexport type ObservedSpanOptions = {\n mapException?: SpanExceptionMapper;\n};\n\nexport function getActiveTraceId(): string | undefined {\n return trace.getSpan(context.active())?.spanContext().traceId;\n}\n\nexport async function withObservedSpan<T>(\n name: string,\n attributes: Attributes,\n work: () => Promise<T>,\n options: ObservedSpanOptions = {},\n): Promise<T> {\n const tracer = trace.getTracer(TRACER_NAME);\n\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n try {\n const result = await work();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (options.mapException) {\n try {\n const mappedException = normalizeMappedException(\n options.mapException(error),\n );\n if (mappedException !== undefined) {\n span.recordException(mappedException);\n }\n } catch {\n // Never fall back to the original value after a mapper is supplied.\n }\n } else {\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n });\n}\n\nfunction normalizeMappedException(\n exception: Exception | undefined,\n): Exception | undefined {\n if (typeof exception === \"string\") {\n return boundedExceptionField(exception);\n }\n if (!exception || typeof exception !== \"object\") {\n return undefined;\n }\n\n const code = typeof exception.code === \"number\"\n ? exception.code\n : boundedExceptionField(exception.code);\n const message = boundedExceptionField(exception.message);\n const name = boundedExceptionField(exception.name);\n const stack = boundedExceptionField(exception.stack);\n\n if (code === undefined && message === undefined && name === undefined) {\n return undefined;\n }\n\n return {\n ...(code !== undefined ? { code } : {}),\n ...(message !== undefined ? { message } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(stack !== undefined ? { stack } : {}),\n } as Exception;\n}\n\nfunction boundedExceptionField(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const bounded = value.trim().slice(0, MAX_MAPPED_EXCEPTION_FIELD_LENGTH);\n return bounded || undefined;\n}"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP,IAAM,cAAc;AACpB,IAAM,oCAAoC;AAQnC,SAAS,mBAAuC;AACrD,SAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC,GAAG,YAAY,EAAE;AACxD;AAEA,eAAsB,iBACpB,MACA,YACA,MACA,UAA+B,CAAC,GACpB;AACZ,QAAM,SAAS,MAAM,UAAU,WAAW;AAE1C,SAAO,OAAO,gBAAgB,MAAM,EAAE,WAAW,GAAG,OAAO,SAAS;AAClE,QAAI;AACF,YAAM,SAAS,MAAM,KAAK;AAC1B,WAAK,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;AAC1C,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,QAAQ,cAAc;AACxB,YAAI;AACF,gBAAM,kBAAkB;AAAA,YACtB,QAAQ,aAAa,KAAK;AAAA,UAC5B;AACA,cAAI,oBAAoB,QAAW;AACjC,iBAAK,gBAAgB,eAAe;AAAA,UACtC;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF,OAAO;AACL,aAAK;AAAA,UACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAC1D;AAAA,MACF;AACA,WAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;AAC7C,YAAM;AAAA,IACR,UAAE;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF,CAAC;AACH;AAEA,SAAS,yBACP,WACuB;AACvB,MAAI,OAAO,cAAc,UAAU;AACjC,WAAO,sBAAsB,SAAS;AAAA,EACxC;AACA,MAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,UAAU,SAAS,WACnC,UAAU,OACV,sBAAsB,UAAU,IAAI;AACxC,QAAM,UAAU,sBAAsB,UAAU,OAAO;AACvD,QAAM,OAAO,sBAAsB,UAAU,IAAI;AACjD,QAAM,QAAQ,sBAAsB,UAAU,KAAK;AAEnD,MAAI,SAAS,UAAa,YAAY,UAAa,SAAS,QAAW;AACrE,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,EACzC;AACF;AAEA,SAAS,sBAAsB,OAAoC;AACjE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK,EAAE,MAAM,GAAG,iCAAiC;AACvE,SAAO,WAAW;AACpB;","names":[]}
@@ -1,10 +1,17 @@
1
+ import { ObservedSpanOptions } from './index.js';
2
+ export { SpanExceptionMapper } from './index.js';
3
+ import '@opentelemetry/api';
4
+
1
5
  type AgentObservation = {
2
6
  agentName: string;
3
7
  provider?: string;
4
8
  model?: string;
5
9
  };
6
- declare function observeConversationTurn<T>(channel: "whatsapp" | "other", work: () => Promise<T>): Promise<T>;
7
- declare function observeAgentInvocation<T>(observation: AgentObservation, work: () => Promise<T>): Promise<T>;
8
- declare function observeAgentTool<T>(toolName: string, work: () => Promise<T>): Promise<T>;
10
+ type GenAIObservationOptions = ObservedSpanOptions & {
11
+ recordMetrics?: boolean;
12
+ };
13
+ declare function observeConversationTurn<T>(channel: "whatsapp" | "other", work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
14
+ declare function observeAgentInvocation<T>(observation: AgentObservation, work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
15
+ declare function observeAgentTool<T>(toolName: string, work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
9
16
 
10
- export { type AgentObservation, observeAgentInvocation, observeAgentTool, observeConversationTurn };
17
+ export { type AgentObservation, type GenAIObservationOptions, ObservedSpanOptions, observeAgentInvocation, observeAgentTool, observeConversationTurn };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withObservedSpan
3
- } from "../chunk-XH6XHKXC.js";
3
+ } from "../chunk-A3EK6N46.js";
4
4
 
5
5
  // src/observability/genai.ts
6
6
  import { metrics } from "@opentelemetry/api";
@@ -22,52 +22,64 @@ var turnDuration = meter.createHistogram(
22
22
  { unit: "ms" }
23
23
  );
24
24
  var turnOperations = meter.createCounter("moda.conversation.turn.operations");
25
- async function observeConversationTurn(channel, work) {
26
- return observeOperation(
25
+ async function observeConversationTurn(channel, work, options = {}) {
26
+ const observeSpan = () => withObservedSpan(
27
+ `conversation.turn ${channel}`,
28
+ { "moda.messaging.channel": channel },
29
+ work,
30
+ options
31
+ );
32
+ return observeGenAIOperation(
27
33
  turnDuration,
28
34
  turnOperations,
29
35
  { channel },
30
- () => withObservedSpan(
31
- `conversation.turn ${channel}`,
32
- { "moda.messaging.channel": channel },
33
- work
34
- )
36
+ observeSpan,
37
+ options
35
38
  );
36
39
  }
37
- async function observeAgentInvocation(observation, work) {
40
+ async function observeAgentInvocation(observation, work, options = {}) {
38
41
  const agentName = boundedSpanValue(observation.agentName) ?? "unknown";
39
- return observeOperation(
42
+ const observeSpan = () => withObservedSpan(
43
+ `invoke_agent ${agentName}`,
44
+ compact({
45
+ "gen_ai.operation.name": "invoke_agent",
46
+ "gen_ai.agent.name": agentName,
47
+ "gen_ai.provider.name": boundedSpanValue(observation.provider),
48
+ "gen_ai.request.model": boundedSpanValue(observation.model)
49
+ }),
50
+ work,
51
+ options
52
+ );
53
+ return observeGenAIOperation(
40
54
  agentDuration,
41
55
  agentOperations,
42
56
  {},
43
- () => withObservedSpan(
44
- `invoke_agent ${agentName}`,
45
- compact({
46
- "gen_ai.operation.name": "invoke_agent",
47
- "gen_ai.agent.name": agentName,
48
- "gen_ai.provider.name": boundedSpanValue(observation.provider),
49
- "gen_ai.request.model": boundedSpanValue(observation.model)
50
- }),
51
- work
52
- )
57
+ observeSpan,
58
+ options
53
59
  );
54
60
  }
55
- async function observeAgentTool(toolName, work) {
61
+ async function observeAgentTool(toolName, work, options = {}) {
56
62
  const safeToolName = boundedSpanValue(toolName) ?? "unknown";
57
- return observeOperation(
63
+ const observeSpan = () => withObservedSpan(
64
+ `execute_tool ${safeToolName}`,
65
+ {
66
+ "gen_ai.operation.name": "execute_tool",
67
+ "gen_ai.tool.name": safeToolName
68
+ },
69
+ work,
70
+ options
71
+ );
72
+ return observeGenAIOperation(
58
73
  toolDuration,
59
74
  toolOperations,
60
75
  {},
61
- () => withObservedSpan(
62
- `execute_tool ${safeToolName}`,
63
- {
64
- "gen_ai.operation.name": "execute_tool",
65
- "gen_ai.tool.name": safeToolName
66
- },
67
- work
68
- )
76
+ observeSpan,
77
+ options
69
78
  );
70
79
  }
80
+ function observeGenAIOperation(duration, operations, attributes, work, options) {
81
+ return options.recordMetrics === false ? work() : observeOperation(duration, operations, attributes, work);
82
+ }
71
83
  async function observeOperation(duration, operations, attributes, work) {
72
84
  const started = performance.now();
73
85
  let outcome = "error";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/observability/genai.ts"],"sourcesContent":["import { metrics, type Attributes, type Counter, type Histogram } from \"@opentelemetry/api\";\nimport { withObservedSpan } from \"./index.js\";\n\nconst MAX_ATTRIBUTE_LENGTH = 80;\nconst meter = metrics.getMeter(\n \"@modainteract/moda-interact-shared/observability/genai\",\n);\nconst agentDuration = meter.createHistogram(\n \"moda.agent.invocation.duration_ms\",\n { unit: \"ms\" },\n);\nconst agentOperations = meter.createCounter(\"moda.agent.invocation.operations\");\nconst toolDuration = meter.createHistogram(\"moda.agent.tool.duration_ms\", {\n unit: \"ms\",\n});\nconst toolOperations = meter.createCounter(\"moda.agent.tool.operations\");\nconst turnDuration = meter.createHistogram(\n \"moda.conversation.turn.duration_ms\",\n { unit: \"ms\" },\n);\nconst turnOperations = meter.createCounter(\"moda.conversation.turn.operations\");\n\ntype Outcome = \"success\" | \"error\";\n\nexport type AgentObservation = {\n agentName: string;\n provider?: string;\n model?: string;\n};\n\nexport async function observeConversationTurn<T>(\n channel: \"whatsapp\" | \"other\",\n work: () => Promise<T>,\n): Promise<T> {\n return observeOperation(\n turnDuration,\n turnOperations,\n { channel },\n () => withObservedSpan(\n `conversation.turn ${channel}`,\n { \"moda.messaging.channel\": channel },\n work,\n ),\n );\n}\n\nexport async function observeAgentInvocation<T>(\n observation: AgentObservation,\n work: () => Promise<T>,\n): Promise<T> {\n const agentName = boundedSpanValue(observation.agentName) ?? \"unknown\";\n\n return observeOperation(\n agentDuration,\n agentOperations,\n {},\n () => withObservedSpan(\n `invoke_agent ${agentName}`,\n compact({\n \"gen_ai.operation.name\": \"invoke_agent\",\n \"gen_ai.agent.name\": agentName,\n \"gen_ai.provider.name\": boundedSpanValue(observation.provider),\n \"gen_ai.request.model\": boundedSpanValue(observation.model),\n }),\n work,\n ),\n );\n}\n\nexport async function observeAgentTool<T>(\n toolName: string,\n work: () => Promise<T>,\n): Promise<T> {\n const safeToolName = boundedSpanValue(toolName) ?? \"unknown\";\n\n return observeOperation(\n toolDuration,\n toolOperations,\n {},\n () => withObservedSpan(\n `execute_tool ${safeToolName}`,\n {\n \"gen_ai.operation.name\": \"execute_tool\",\n \"gen_ai.tool.name\": safeToolName,\n },\n work,\n ),\n );\n}\n\nasync function observeOperation<T>(\n duration: Histogram,\n operations: Counter,\n attributes: Attributes,\n work: () => Promise<T>,\n): Promise<T> {\n const started = performance.now();\n let outcome: Outcome = \"error\";\n\n try {\n const result = await work();\n outcome = \"success\";\n return result;\n } finally {\n recordMetrics(duration, operations, performance.now() - started, {\n ...attributes,\n outcome,\n });\n }\n}\n\nfunction recordMetrics(\n duration: Histogram,\n operations: Counter,\n elapsedMilliseconds: number,\n attributes: Attributes,\n): void {\n try {\n duration.record(elapsedMilliseconds, attributes);\n operations.add(1, attributes);\n } catch {\n // Telemetry must not replace an application result or error.\n }\n}\n\nfunction boundedSpanValue(value: string | undefined): string | undefined {\n if (value === undefined) return undefined;\n const bounded = value.trim().slice(0, MAX_ATTRIBUTE_LENGTH);\n return bounded || undefined;\n}\n\nfunction compact(\n attributes: Record<string, string | undefined>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(attributes).filter((entry): entry is [string, string] =>\n entry[1] !== undefined\n ),\n );\n}"],"mappings":";;;;;AAAA,SAAS,eAA8D;AAGvE,IAAM,uBAAuB;AAC7B,IAAM,QAAQ,QAAQ;AAAA,EACpB;AACF;AACA,IAAM,gBAAgB,MAAM;AAAA,EAC1B;AAAA,EACA,EAAE,MAAM,KAAK;AACf;AACA,IAAM,kBAAkB,MAAM,cAAc,kCAAkC;AAC9E,IAAM,eAAe,MAAM,gBAAgB,+BAA+B;AAAA,EACxE,MAAM;AACR,CAAC;AACD,IAAM,iBAAiB,MAAM,cAAc,4BAA4B;AACvE,IAAM,eAAe,MAAM;AAAA,EACzB;AAAA,EACA,EAAE,MAAM,KAAK;AACf;AACA,IAAM,iBAAiB,MAAM,cAAc,mCAAmC;AAU9E,eAAsB,wBACpB,SACA,MACY;AACZ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,QAAQ;AAAA,IACV,MAAM;AAAA,MACJ,qBAAqB,OAAO;AAAA,MAC5B,EAAE,0BAA0B,QAAQ;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,uBACpB,aACA,MACY;AACZ,QAAM,YAAY,iBAAiB,YAAY,SAAS,KAAK;AAE7D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,MAAM;AAAA,MACJ,gBAAgB,SAAS;AAAA,MACzB,QAAQ;AAAA,QACN,yBAAyB;AAAA,QACzB,qBAAqB;AAAA,QACrB,wBAAwB,iBAAiB,YAAY,QAAQ;AAAA,QAC7D,wBAAwB,iBAAiB,YAAY,KAAK;AAAA,MAC5D,CAAC;AAAA,MACD;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,iBACpB,UACA,MACY;AACZ,QAAM,eAAe,iBAAiB,QAAQ,KAAK;AAEnD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,MAAM;AAAA,MACJ,gBAAgB,YAAY;AAAA,MAC5B;AAAA,QACE,yBAAyB;AAAA,QACzB,oBAAoB;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAe,iBACb,UACA,YACA,YACA,MACY;AACZ,QAAM,UAAU,YAAY,IAAI;AAChC,MAAI,UAAmB;AAEvB,MAAI;AACF,UAAM,SAAS,MAAM,KAAK;AAC1B,cAAU;AACV,WAAO;AAAA,EACT,UAAE;AACA,kBAAc,UAAU,YAAY,YAAY,IAAI,IAAI,SAAS;AAAA,MAC/D,GAAG;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,cACP,UACA,YACA,qBACA,YACM;AACN,MAAI;AACF,aAAS,OAAO,qBAAqB,UAAU;AAC/C,eAAW,IAAI,GAAG,UAAU;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,iBAAiB,OAA+C;AACvE,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,UAAU,MAAM,KAAK,EAAE,MAAM,GAAG,oBAAoB;AAC1D,SAAO,WAAW;AACpB;AAEA,SAAS,QACP,YACwB;AACxB,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,UAAU,EAAE;AAAA,MAAO,CAAC,UACjC,MAAM,CAAC,MAAM;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/observability/genai.ts"],"sourcesContent":["import { metrics, type Attributes, type Counter, type Histogram } from \"@opentelemetry/api\";\nimport {\n withObservedSpan,\n type ObservedSpanOptions,\n} from \"./index.js\";\n\nexport type {\n ObservedSpanOptions,\n SpanExceptionMapper,\n} from \"./index.js\";\n\nconst MAX_ATTRIBUTE_LENGTH = 80;\nconst meter = metrics.getMeter(\n \"@modainteract/moda-interact-shared/observability/genai\",\n);\nconst agentDuration = meter.createHistogram(\n \"moda.agent.invocation.duration_ms\",\n { unit: \"ms\" },\n);\nconst agentOperations = meter.createCounter(\"moda.agent.invocation.operations\");\nconst toolDuration = meter.createHistogram(\"moda.agent.tool.duration_ms\", {\n unit: \"ms\",\n});\nconst toolOperations = meter.createCounter(\"moda.agent.tool.operations\");\nconst turnDuration = meter.createHistogram(\n \"moda.conversation.turn.duration_ms\",\n { unit: \"ms\" },\n);\nconst turnOperations = meter.createCounter(\"moda.conversation.turn.operations\");\n\ntype Outcome = \"success\" | \"error\";\n\nexport type AgentObservation = {\n agentName: string;\n provider?: string;\n model?: string;\n};\n\nexport type GenAIObservationOptions = ObservedSpanOptions & {\n recordMetrics?: boolean;\n};\n\nexport async function observeConversationTurn<T>(\n channel: \"whatsapp\" | \"other\",\n work: () => Promise<T>,\n options: GenAIObservationOptions = {},\n): Promise<T> {\n const observeSpan = () => withObservedSpan(\n `conversation.turn ${channel}`,\n { \"moda.messaging.channel\": channel },\n work,\n options,\n );\n\n return observeGenAIOperation(\n turnDuration,\n turnOperations,\n { channel },\n observeSpan,\n options,\n );\n}\n\nexport async function observeAgentInvocation<T>(\n observation: AgentObservation,\n work: () => Promise<T>,\n options: GenAIObservationOptions = {},\n): Promise<T> {\n const agentName = boundedSpanValue(observation.agentName) ?? \"unknown\";\n\n const observeSpan = () => withObservedSpan(\n `invoke_agent ${agentName}`,\n compact({\n \"gen_ai.operation.name\": \"invoke_agent\",\n \"gen_ai.agent.name\": agentName,\n \"gen_ai.provider.name\": boundedSpanValue(observation.provider),\n \"gen_ai.request.model\": boundedSpanValue(observation.model),\n }),\n work,\n options,\n );\n\n return observeGenAIOperation(\n agentDuration,\n agentOperations,\n {},\n observeSpan,\n options,\n );\n}\n\nexport async function observeAgentTool<T>(\n toolName: string,\n work: () => Promise<T>,\n options: GenAIObservationOptions = {},\n): Promise<T> {\n const safeToolName = boundedSpanValue(toolName) ?? \"unknown\";\n\n const observeSpan = () => withObservedSpan(\n `execute_tool ${safeToolName}`,\n {\n \"gen_ai.operation.name\": \"execute_tool\",\n \"gen_ai.tool.name\": safeToolName,\n },\n work,\n options,\n );\n\n return observeGenAIOperation(\n toolDuration,\n toolOperations,\n {},\n observeSpan,\n options,\n );\n}\n\nfunction observeGenAIOperation<T>(\n duration: Histogram,\n operations: Counter,\n attributes: Attributes,\n work: () => Promise<T>,\n options: GenAIObservationOptions,\n): Promise<T> {\n return options.recordMetrics === false\n ? work()\n : observeOperation(duration, operations, attributes, work);\n}\n\nasync function observeOperation<T>(\n duration: Histogram,\n operations: Counter,\n attributes: Attributes,\n work: () => Promise<T>,\n): Promise<T> {\n const started = performance.now();\n let outcome: Outcome = \"error\";\n\n try {\n const result = await work();\n outcome = \"success\";\n return result;\n } finally {\n recordMetrics(duration, operations, performance.now() - started, {\n ...attributes,\n outcome,\n });\n }\n}\n\nfunction recordMetrics(\n duration: Histogram,\n operations: Counter,\n elapsedMilliseconds: number,\n attributes: Attributes,\n): void {\n try {\n duration.record(elapsedMilliseconds, attributes);\n operations.add(1, attributes);\n } catch {\n // Telemetry must not replace an application result or error.\n }\n}\n\nfunction boundedSpanValue(value: string | undefined): string | undefined {\n if (value === undefined) return undefined;\n const bounded = value.trim().slice(0, MAX_ATTRIBUTE_LENGTH);\n return bounded || undefined;\n}\n\nfunction compact(\n attributes: Record<string, string | undefined>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(attributes).filter((entry): entry is [string, string] =>\n entry[1] !== undefined\n ),\n );\n}"],"mappings":";;;;;AAAA,SAAS,eAA8D;AAWvE,IAAM,uBAAuB;AAC7B,IAAM,QAAQ,QAAQ;AAAA,EACpB;AACF;AACA,IAAM,gBAAgB,MAAM;AAAA,EAC1B;AAAA,EACA,EAAE,MAAM,KAAK;AACf;AACA,IAAM,kBAAkB,MAAM,cAAc,kCAAkC;AAC9E,IAAM,eAAe,MAAM,gBAAgB,+BAA+B;AAAA,EACxE,MAAM;AACR,CAAC;AACD,IAAM,iBAAiB,MAAM,cAAc,4BAA4B;AACvE,IAAM,eAAe,MAAM;AAAA,EACzB;AAAA,EACA,EAAE,MAAM,KAAK;AACf;AACA,IAAM,iBAAiB,MAAM,cAAc,mCAAmC;AAc9E,eAAsB,wBACpB,SACA,MACA,UAAmC,CAAC,GACxB;AACZ,QAAM,cAAc,MAAM;AAAA,IACxB,qBAAqB,OAAO;AAAA,IAC5B,EAAE,0BAA0B,QAAQ;AAAA,IACpC;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,QAAQ;AAAA,IACV;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,uBACpB,aACA,MACA,UAAmC,CAAC,GACxB;AACZ,QAAM,YAAY,iBAAiB,YAAY,SAAS,KAAK;AAE7D,QAAM,cAAc,MAAM;AAAA,IACxB,gBAAgB,SAAS;AAAA,IACzB,QAAQ;AAAA,MACN,yBAAyB;AAAA,MACzB,qBAAqB;AAAA,MACrB,wBAAwB,iBAAiB,YAAY,QAAQ;AAAA,MAC7D,wBAAwB,iBAAiB,YAAY,KAAK;AAAA,IAC5D,CAAC;AAAA,IACD;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,iBACpB,UACA,MACA,UAAmC,CAAC,GACxB;AACZ,QAAM,eAAe,iBAAiB,QAAQ,KAAK;AAEnD,QAAM,cAAc,MAAM;AAAA,IACxB,gBAAgB,YAAY;AAAA,IAC5B;AAAA,MACE,yBAAyB;AAAA,MACzB,oBAAoB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,sBACP,UACA,YACA,YACA,MACA,SACY;AACZ,SAAO,QAAQ,kBAAkB,QAC7B,KAAK,IACL,iBAAiB,UAAU,YAAY,YAAY,IAAI;AAC7D;AAEA,eAAe,iBACb,UACA,YACA,YACA,MACY;AACZ,QAAM,UAAU,YAAY,IAAI;AAChC,MAAI,UAAmB;AAEvB,MAAI;AACF,UAAM,SAAS,MAAM,KAAK;AAC1B,cAAU;AACV,WAAO;AAAA,EACT,UAAE;AACA,kBAAc,UAAU,YAAY,YAAY,IAAI,IAAI,SAAS;AAAA,MAC/D,GAAG;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,cACP,UACA,YACA,qBACA,YACM;AACN,MAAI;AACF,aAAS,OAAO,qBAAqB,UAAU;AAC/C,eAAW,IAAI,GAAG,UAAU;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,iBAAiB,OAA+C;AACvE,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,UAAU,MAAM,KAAK,EAAE,MAAM,GAAG,oBAAoB;AAC1D,SAAO,WAAW;AACpB;AAEA,SAAS,QACP,YACwB;AACxB,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,UAAU,EAAE;AAAA,MAAO,CAAC,UACjC,MAAM,CAAC,MAAM;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
@@ -1,6 +1,10 @@
1
- import { Attributes } from '@opentelemetry/api';
1
+ import { Exception, Attributes } from '@opentelemetry/api';
2
2
 
3
+ type SpanExceptionMapper = (error: unknown) => Exception | undefined;
4
+ type ObservedSpanOptions = {
5
+ mapException?: SpanExceptionMapper;
6
+ };
3
7
  declare function getActiveTraceId(): string | undefined;
4
- declare function withObservedSpan<T>(name: string, attributes: Attributes, work: () => Promise<T>): Promise<T>;
8
+ declare function withObservedSpan<T>(name: string, attributes: Attributes, work: () => Promise<T>, options?: ObservedSpanOptions): Promise<T>;
5
9
 
6
- export { getActiveTraceId, withObservedSpan };
10
+ export { type ObservedSpanOptions, type SpanExceptionMapper, getActiveTraceId, withObservedSpan };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getActiveTraceId,
3
3
  withObservedSpan
4
- } from "../chunk-XH6XHKXC.js";
4
+ } from "../chunk-A3EK6N46.js";
5
5
  export {
6
6
  getActiveTraceId,
7
7
  withObservedSpan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modainteract/moda-interact-shared",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,34 +0,0 @@
1
- // src/observability/index.ts
2
- import {
3
- context,
4
- SpanStatusCode,
5
- trace
6
- } from "@opentelemetry/api";
7
- var TRACER_NAME = "@modainteract/moda-interact-shared/observability";
8
- function getActiveTraceId() {
9
- return trace.getSpan(context.active())?.spanContext().traceId;
10
- }
11
- async function withObservedSpan(name, attributes, work) {
12
- const tracer = trace.getTracer(TRACER_NAME);
13
- return tracer.startActiveSpan(name, { attributes }, async (span) => {
14
- try {
15
- const result = await work();
16
- span.setStatus({ code: SpanStatusCode.OK });
17
- return result;
18
- } catch (error) {
19
- span.recordException(
20
- error instanceof Error ? error : new Error(String(error))
21
- );
22
- span.setStatus({ code: SpanStatusCode.ERROR });
23
- throw error;
24
- } finally {
25
- span.end();
26
- }
27
- });
28
- }
29
-
30
- export {
31
- getActiveTraceId,
32
- withObservedSpan
33
- };
34
- //# sourceMappingURL=chunk-XH6XHKXC.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/observability/index.ts"],"sourcesContent":["import {\n context,\n SpanStatusCode,\n trace,\n type Attributes,\n} from \"@opentelemetry/api\";\n\nconst TRACER_NAME = \"@modainteract/moda-interact-shared/observability\";\n\nexport function getActiveTraceId(): string | undefined {\n return trace.getSpan(context.active())?.spanContext().traceId;\n}\n\nexport async function withObservedSpan<T>(\n name: string,\n attributes: Attributes,\n work: () => Promise<T>,\n): Promise<T> {\n const tracer = trace.getTracer(TRACER_NAME);\n\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n try {\n const result = await work();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n });\n}"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,IAAM,cAAc;AAEb,SAAS,mBAAuC;AACrD,SAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC,GAAG,YAAY,EAAE;AACxD;AAEA,eAAsB,iBACpB,MACA,YACA,MACY;AACZ,QAAM,SAAS,MAAM,UAAU,WAAW;AAE1C,SAAO,OAAO,gBAAgB,MAAM,EAAE,WAAW,GAAG,OAAO,SAAS;AAClE,QAAI;AACF,YAAM,SAAS,MAAM,KAAK;AAC1B,WAAK,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;AAC1C,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK;AAAA,QACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MAC1D;AACA,WAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;AAC7C,YAAM;AAAA,IACR,UAAE;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF,CAAC;AACH;","names":[]}