import type { Attributes } from "@opentelemetry/api"; import { RandomIdGenerator } from "@opentelemetry/sdk-trace-base"; import { parseTraceparent } from "@trigger.dev/core/v3/isomorphic"; import type { ExceptionEventProperties, SpanEvents, TaskRunError, } from "@trigger.dev/core/v3/schemas"; import { unflattenAttributes } from "@trigger.dev/core/v3/utils/flattenAttributes"; import { createHash } from "node:crypto"; export function extractContextFromCarrier(carrier: Record) { const traceparent = carrier["traceparent"]; const tracestate = carrier["tracestate"]; if (typeof traceparent !== "string") { return undefined; } return { ...carrier, traceparent: parseTraceparent(traceparent), tracestate, }; } export function getNowInNanoseconds(): bigint { return BigInt(new Date().getTime() * 1_000_000); } export function getDateFromNanoseconds(nanoseconds: bigint): Date { return new Date(Number(nanoseconds) / 1_000_000); } export function calculateDurationFromStart( startTime: bigint, endTime: Date = new Date(), minimumDuration?: number ) { const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime; const duration = Number(BigInt($endtime.getTime() * 1_000_000) - startTime); if (minimumDuration && duration < minimumDuration) { return minimumDuration; } return duration; } export function calculateDurationFromStartJsDate(startTime: Date, endTime: Date = new Date()) { const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime; return ($endtime.getTime() - startTime.getTime()) * 1_000_000; } export function convertDateToNanoseconds(date: Date): bigint { return BigInt(date.getTime()) * BigInt(1_000_000); } /** * Returns a deterministically random 8-byte span ID formatted/encoded as a 16 lowercase hex * characters corresponding to 64 bits, based on the trace ID and seed. */ export function generateDeterministicSpanId(traceId: string, seed: string) { const hash = createHash("sha1"); hash.update(traceId); hash.update(seed); const buffer = hash.digest(); let hexString = ""; for (let i = 0; i < 8; i++) { const val = buffer.readUInt8(i); const str = val.toString(16).padStart(2, "0"); hexString += str; } return hexString; } const randomIdGenerator = new RandomIdGenerator(); export function generateTraceId() { return randomIdGenerator.generateTraceId(); } export function generateSpanId() { return randomIdGenerator.generateSpanId(); } export function stripAttributePrefix(attributes: Attributes, prefix: string) { const result: Attributes = {}; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith(prefix)) { result[key.slice(prefix.length + 1)] = value; } else { result[key] = value; } } return result; } export function parseEventsField(events: unknown): SpanEvents { if (!events) return []; if (!Array.isArray(events)) return []; const unsafe = events ? (events as any[]).map((e) => ({ ...e, properties: unflattenAttributes(e.properties as Attributes), })) : undefined; return unsafe as SpanEvents; } export function createExceptionPropertiesFromError(error: TaskRunError): ExceptionEventProperties { switch (error.type) { case "BUILT_IN_ERROR": { return { type: error.name, message: error.message, stacktrace: error.stackTrace, }; } case "CUSTOM_ERROR": { return { type: "Error", message: error.raw, }; } case "INTERNAL_ERROR": { return { type: "Internal error", message: [error.code, error.message].filter(Boolean).join(": "), stacktrace: error.stackTrace, }; } case "STRING_ERROR": { return { type: "Error", message: error.raw, }; } } } // Removes internal/private attribute keys from span properties. // Filters: "$" prefixed keys (private metadata) and "ctx." prefixed keys (Trigger.dev run context) export function removePrivateProperties( attributes: Attributes | undefined | null ): Attributes | undefined { if (!attributes) { return undefined; } const result: Attributes = {}; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith("$") || key.startsWith("ctx.")) { continue; } result[key] = value; } if (Object.keys(result).length === 0) { return undefined; } return result; } export function isEmptyObject(obj: object) { for (var prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { return false; } } return true; }