Skip to content

Commit 7f5c449

Browse files
committed
update
1 parent 746157c commit 7f5c449

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

apps/sim/lib/copilot/request/go/fetch.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type Context, context, SpanStatusCode, trace } from '@opentelemetry/api
22
import { CopilotLeg } from '@/lib/copilot/generated/trace-attribute-values-v1'
33
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
44
import { traceHeaders } from '@/lib/copilot/request/go/propagation'
5-
import { markSpanForError } from '@/lib/copilot/request/otel'
5+
import { isActionableErrorStatus, markSpanForError } from '@/lib/copilot/request/otel'
66

77
// Lazy tracer resolution: module-level `trace.getTracer()` can be evaluated
88
// before `instrumentation-node.ts` installs the TracerProvider under
@@ -80,7 +80,12 @@ export async function fetchGo(url: string, options: OutboundFetchOptions = {}):
8080
if (contentLength > 0) {
8181
span.setAttribute(TraceAttr.HttpResponseContentLength, contentLength)
8282
}
83-
if (response.status >= 400) {
83+
// Only mark ERROR for actionable status codes. 4xx that represent
84+
// normal auth/validation rejections (400/401/403/404/405/422/etc.)
85+
// stay UNSET so error dashboards don't drown in expected rejection
86+
// paths. See `isActionableErrorStatus` in Go's telemetry middleware
87+
// for the mirror rule (5xx + 402/409/429).
88+
if (isActionableErrorStatus(response.status)) {
8489
span.setStatus({
8590
code: SpanStatusCode.ERROR,
8691
message: `HTTP ${response.status}`,

apps/sim/lib/copilot/request/otel.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ function isGenAIMessageCaptureEnabled(): boolean {
3737
// flavor). Callers suppress ERROR status on cancel paths.
3838
export function isCancellationError(err: unknown): boolean {
3939
if (err == null) return false
40+
// `controller.abort(reason)` where `reason` is a plain string
41+
// rejects fetch() with that string directly, not a DOMException.
42+
// Our abort taxonomy uses `user_stop:*`, `redis_abort_marker:*`,
43+
// `timeout:*` reason prefixes (see `request/session/abort.ts`), so
44+
// treat those strings — and any fetch-abort-flavored message — as
45+
// cancellation. Without this branch, a user Stop turns into a
46+
// span with status=error even though nothing went wrong.
47+
if (typeof err === 'string') {
48+
return /aborted|AbortError|^user_stop:|^redis_abort_marker:|^timeout:/i.test(err)
49+
}
4050
if (typeof err === 'object') {
4151
const e = err as { name?: unknown; code?: unknown; message?: unknown }
4252
if (e.name === 'AbortError') return true
@@ -49,6 +59,24 @@ export function isCancellationError(err: unknown): boolean {
4959
return false
5060
}
5161

62+
/**
63+
* True iff an HTTP response status code represents a real server-side
64+
* problem (5xx) or a user-visible condition we want to alert on
65+
* (402 Payment Required, 409 Conflict, 429 Too Many Requests).
66+
*
67+
* Everything else — in particular the 4xx flood from bot probes and
68+
* expected auth/validation rejections — stays UNSET on the span so
69+
* dashboards don't treat normal rejections as errors.
70+
*
71+
* Mirrored on the Go side in
72+
* `copilot/internal/http/middleware/telemetry.go`. Keep the two in
73+
* sync if you change the actionable set.
74+
*/
75+
export function isActionableErrorStatus(code: number): boolean {
76+
if (code >= 500) return true
77+
return code === 402 || code === 409 || code === 429
78+
}
79+
5280
// Record exception + set ERROR only for real failures (cancels stay unset).
5381
export function markSpanForError(span: Span, error: unknown): void {
5482
const asError = error instanceof Error ? error : new Error(String(error))

0 commit comments

Comments
 (0)