Skip to content

Instantly share code, notes, and snippets.

@evelant
Created January 29, 2024 17:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evelant/35adaba99760056d3107503fa3ab806d to your computer and use it in GitHub Desktop.
Save evelant/35adaba99760056d3107503fa3ab806d to your computer and use it in GitHub Desktop.
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