Created
January 29, 2024 17:36
-
-
Save evelant/35adaba99760056d3107503fa3ab806d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Span as SentrySpan, | |
SpanStatusType, | |
getActiveSpan, | |
getActiveTransaction, | |
getCurrentHub, | |
startSpanManual, | |
startTransaction, | |
} from "@sentry/core" | |
import type * as Sentry from "@sentry/types" | |
import { Cause, Context, Effect, Exit, FiberRef, Layer, Option, Tracer } from "effect" | |
const EffectSentrySpanTypeId = Symbol.for("@effect/Tracer/SentrySpan") | |
const bigintToSentryTimestamp = (x: bigint) => Number(x / 1000000n) / 1000 | |
export interface SentryTracingApi { | |
startSpanManual: typeof startSpanManual | |
getActiveTransaction: typeof getActiveTransaction | |
getActiveSpan: typeof getActiveSpan | |
} | |
let DEBUG_TRACER = false | |
export class EffectSentrySpan implements Tracer.Span { | |
readonly [EffectSentrySpanTypeId]: typeof EffectSentrySpanTypeId | |
readonly _tag = "Span" | |
readonly span: SentrySpan | |
readonly spanId: string | |
readonly traceId: string | |
readonly attributes = new Map<string, unknown>() | |
readonly sampled: boolean | |
status: Tracer.SpanStatus | |
constructor( | |
readonly name: string, | |
readonly parent: Option.Option<Tracer.ParentSpan>, | |
readonly context: Context.Context<never>, | |
readonly links: ReadonlyArray<Tracer.SpanLink>, | |
startTime: bigint, | |
) { | |
this[EffectSentrySpanTypeId] = EffectSentrySpanTypeId | |
try { | |
const startTimestamp = bigintToSentryTimestamp(startTime) | |
const activeSpan = getActiveSpan() | |
if (DEBUG_TRACER) | |
console.log( | |
`TRACING new sentry span `, | |
name, | |
Option.isSome(parent), | |
Option.getOrElse(parent, () => "no_parent"), | |
activeSpan, | |
// sentryTransaction, | |
// sentryTransaction?.name, | |
new Date(startTimestamp), | |
links, | |
context, | |
) | |
if (links.length > 0) { | |
console.log(`TRACING span has links `, links) | |
} | |
if (Option.isSome(parent)) { | |
if (parent.value instanceof EffectSentrySpan) { | |
this.span = parent.value.span.startChild({ op: `${name}`, startTimestamp: startTimestamp }) | |
} else { | |
console.warn(`parent span is not an EffectSentrySpan`, parent.value) | |
this.span = startTransaction({ | |
name: `${name}`, | |
startTimestamp: startTimestamp, | |
parentSpanId: parent.value.spanId, | |
traceId: parent.value.traceId, | |
}) | |
} | |
} else { | |
if (DEBUG_TRACER) console.log(`no parent span, starting transaction ${name}`) | |
this.span = startTransaction({ name: `${name}`, startTimestamp: startTimestamp }) | |
} | |
const hub = getCurrentHub() | |
const scope = hub.getScope() | |
scope.setSpan(this.span) | |
} catch (e: any) { | |
console.error(`error starting sentry span`, e) | |
throw e | |
} | |
this.spanId = this.span.spanId | |
this.traceId = this.span.traceId | |
this.status = { | |
_tag: "Started", | |
startTime, | |
} | |
this.sampled = !!this.span.sampled | |
if (DEBUG_TRACER) console.log(`TRACING done building new sentry span`, name) | |
} | |
attribute(key: string, value: unknown) { | |
try { | |
if (DEBUG_TRACER) console.log(`TRACING sentry span attribute `, this.span, key, value) | |
this.span.setData(key, unknownToAttributeValue(value)) | |
this.attributes.set(key, value) | |
} catch (e: any) { | |
console.error(`error setting sentry span attribute`, e) | |
} | |
} | |
end(endTime: bigint, exit: Exit.Exit<unknown, unknown>) { | |
try { | |
this.status = { | |
_tag: "Ended", | |
endTime, | |
exit, | |
startTime: this.status.startTime, | |
} | |
if (exit._tag === "Success") { | |
this.span.setStatus("ok" as SpanStatusType) | |
} else { | |
if (Cause.isInterruptedOnly(exit.cause)) { | |
this.span.setStatus("aborted" as SpanStatusType) | |
this.span.setData("cause", Cause.pretty(exit.cause)) | |
this.span.setData("span.label", "⚠︎ Interrupted") | |
this.span.setData("status.interrupted", true) | |
} else { | |
this.span.setStatus("internal_error" as SpanStatusType) | |
this.span.setData("cause", Cause.pretty(exit.cause)) | |
this.span.setData("span.label", "⚠︎ Error") | |
this.span.setData("status.error", true) | |
} | |
} | |
const endTimestamp = bigintToSentryTimestamp(endTime) | |
this.span.finish(endTimestamp) | |
const trans = this.span.transaction | |
if (DEBUG_TRACER) console.log(`TRACING span end `, this.name, this.span, this.span.transaction?.name) | |
} catch (e: any) { | |
console.error(`error ending sentry span`, e) | |
} | |
} | |
event(name: string, startTime: bigint, attributes?: Record<string, unknown>) { | |
try { | |
// const startTimestamp = bigintToSentryTimestamp(startTime) | |
this.span | |
.startChild({ | |
name, | |
// startTimestamp, | |
op: "mark", | |
data: attributes ? recordToAttributes(attributes) : recordToAttributes({}), | |
}) | |
.finish() | |
} catch (e: any) { | |
console.error(`error sending sentry span event`, e) | |
} | |
} | |
} | |
export const makeSentryTracer = Effect.map(Effect.unit, () => { | |
if (DEBUG_TRACER) console.log(`TRACING making sentry tracer`) | |
return Tracer.make({ | |
span(name, parent, context, links, startTime) { | |
return new EffectSentrySpan(name, parent, context, links, startTime) | |
}, | |
context(execution, fiber) { | |
const currentEffectSpan = fiber.getFiberRef(FiberRef.currentContext).unsafeMap.get(Tracer.ParentSpan) as | |
| Tracer.ParentSpan | |
| undefined | |
if (currentEffectSpan === undefined) { | |
return execution() | |
} | |
const hub = getCurrentHub() | |
const scope = hub.getScope() | |
const span = scope.getSpan() | |
// const scopeTransaction = scope.getTransaction() | |
const transaction = getActiveTransaction() | |
const activeSentrySpan = span ?? transaction | |
try { | |
if (!activeSentrySpan) { | |
if (currentEffectSpan instanceof EffectSentrySpan) { | |
if (DEBUG_TRACER) | |
console.log( | |
`TRACING context resume sentry span in EffectSentrySpan ${currentEffectSpan.span.name}`, | |
) | |
// getCurrentHub().getScope().setSpan(currentEffectSpan.span) | |
scope.setSpan(currentEffectSpan.span) | |
} else { | |
console.warn( | |
`TRACING SentryTracer resume context currentSpan is not an EffectSentrySpan`, | |
currentEffectSpan, | |
) | |
} | |
return execution() | |
} else { | |
if (currentEffectSpan instanceof EffectSentrySpan) { | |
if (currentEffectSpan.span !== activeSentrySpan) { | |
// console.log( | |
// `context switch span from ${activeSentrySpan.name} ${activeSentrySpan.op} ${activeSentrySpan.op} to ${currentEffectSpan.span.name} ${currentEffectSpan.span.op}`, | |
// ) | |
scope.setSpan(currentEffectSpan.span) | |
} | |
} else { | |
console.warn( | |
`TRACING SentryTracer resume context currentSpan is not an EffectSentrySpan`, | |
currentEffectSpan, | |
) | |
} | |
return execution() | |
} | |
} catch (e: any) { | |
console.error( | |
`error resuming context in span ${currentEffectSpan?.spanId} ${activeSentrySpan?.name} ${activeSentrySpan?.op} ${activeSentrySpan?.spanId}`, | |
e, | |
) | |
// return execution() | |
throw e | |
} | |
}, | |
}) | |
}) | |
export const sentryTracerLayer = Layer.unwrapEffect(Effect.map(makeSentryTracer, Layer.setTracer)) | |
const recordToAttributes = (value: Record<string, unknown>): Sentry.SpanAttributes => { | |
return Object.entries(value).reduce((acc, [key, value]) => { | |
acc[key] = unknownToAttributeValue(value) | |
return acc | |
}, {} as Sentry.SpanAttributes) | |
} | |
const unknownToAttributeValue = (value: unknown): Sentry.SpanAttributeValue => { | |
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { | |
return value | |
} else if (typeof value === "bigint") { | |
return bigintToSentryTimestamp(value) // Number(value) | |
} | |
return objectToAttribute(value) | |
} | |
const objectToAttribute = (value: unknown): string => { | |
try { | |
return JSON.stringify(value, null, 2) | |
} catch { | |
return String(value) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment