Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export {
} from './logs/logs-utils'
export { toOtlpAnyValue, toOtlpKeyValueList } from './utils/otlp-any-value'
export { osResourceAttributes } from './utils/otlp-resource'
export { assignUserAttributes } from './traces/sanitize'
export { PostHogLogs } from './logs'
export type {
BeforeSendLogFn,
Expand Down Expand Up @@ -90,7 +89,8 @@ export type {
} from './metrics/types'
export { PostHogTraces } from './traces'
export { SyncSpanContextManager } from './traces/context'
export { NOOP_SPAN, inertSpan } from './traces/span'
export { inertSpan, runWithActiveSpan } from './traces/span'
export { resolveTracesConfig } from './traces/config'
export type { ResolvedTracesConfig, SpanContextManager, TraceSdkContext } from './traces/types'
// Same barrel convention as logs and metrics for the user-facing tracing types.
export type {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resolveTracesConfig } from '../traces-defaults'
import { resolveTracesConfig } from './config'

describe('resolveTracesConfig', () => {
it('applies the documented defaults', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assignUserAttributes } from '@posthog/core'
import type { ResolvedTracesConfig, TracesConfig } from '@posthog/core'
import { assignUserAttributes } from '../utils/json-utils'
import type { ResolvedTracesConfig } from './types'
import type { TracesConfig } from '@posthog/types'

// OpenTelemetry's BatchSpanProcessor defaults, which sit comfortably under the
// server's 2 MB body cap.
Expand Down
11 changes: 4 additions & 7 deletions packages/core/src/traces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import type {
TraceSdkContext,
TracesHost,
} from './types'
import { NOOP_SPAN, PostHogSpan, describeError, inertSpan, monotonicNow } from './span'
import { NOOP_SPAN, PostHogSpan, describeError, inertSpan, monotonicNow, runWithActiveSpan } from './span'
import { newSpanId, newTraceId } from './ids'
import { parseTraceparent, sanitizeTracestate } from './traceparent'
import { assignUserAttributes, resolveStartTime, sanitizeName } from './sanitize'
import { resolveStartTime, sanitizeName } from './sanitize'
import { assignUserAttributes } from '../utils/json-utils'
import { buildOtlpSpan, buildOtlpTracesPayload, buildTracesResourceAttributes } from './otlp'
import { isPromise, safeSetTimeout } from '../utils'

Expand Down Expand Up @@ -180,11 +181,7 @@ export class PostHogTraces {
const span = this.startSpan(name, options)

try {
// The shared no-op is never activated, so `getActiveSpan()` inside the
// callback reads null — callbacks should use the handle they're given. A
// pass-through handle is activated, so `getActiveSpan()?.traceparent()`
// still propagates an inbound trace through a service with tracing off.
const result = span === NOOP_SPAN ? fn(span) : this._contextManager.with(span, () => fn(span))
const result = runWithActiveSpan(this._contextManager, span, fn)

if (isPromise(result)) {
return result.then(
Expand Down
36 changes: 0 additions & 36 deletions packages/core/src/traces/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import type { Logger } from '../types'
import type { SpanAttributes, SpanTimeInput } from '@posthog/types'
import { UNSERIALIZABLE_VALUE } from '../utils/json-utils'

const FALLBACK_SPAN_NAME = 'unknown'

Expand Down Expand Up @@ -108,38 +107,3 @@ export function resolveSuppliedTime(
}
return supplied
}

/**
* Copies caller-supplied attributes onto `target`, own enumerable keys only.
*
* Read key by key rather than spread: a getter over a disposed resource or a
* revoked proxy throws on the read itself, before the encoder's guards see it.
*
* @internal Exposed for cross-package use within this SDK; not part of the stable public API.
*/
export function assignUserAttributes<T extends Record<string, any>>(
target: T,
source: Record<string, unknown> | undefined
): T {
if (!source) {
return target
}
let keys: string[] = []
try {
keys = Object.keys(source)
} catch {
keys = []
}
for (const key of keys) {
let value: unknown
try {
value = source[key]
} catch {
value = UNSERIALIZABLE_VALUE
}
// defineProperty, not assignment: `attributes['__proto__'] = v` hits the
// prototype setter and the attribute vanishes.
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
}
return target
}
17 changes: 15 additions & 2 deletions packages/core/src/traces/span.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Span, SpanAttributes, SpanAttributeValue, SpanKind, SpanStatusCode, SpanTimeInput } from '@posthog/types'
import type { Logger } from '../types'
import type { SpanEventRecord, SpanRecord } from './types'
import type { SpanContextManager, SpanEventRecord, SpanRecord } from './types'
import { formatTraceparent, normalizeTraceparent, sanitizeTracestate, TRACE_FLAGS_SAMPLED } from './traceparent'
import { assignUserAttributes, clampEndTime, resolveSuppliedTime, sanitizeName } from './sanitize'
import { clampEndTime, resolveSuppliedTime, sanitizeName } from './sanitize'
import { assignUserAttributes } from '../utils/json-utils'
import { isError } from '../utils'

/**
Expand Down Expand Up @@ -275,6 +276,18 @@ export function inertSpan(options?: { parent?: unknown; tracestate?: unknown }):
return new PassThroughSpan(traceparent, sanitizeTracestate(options?.tracestate))
}

/**
* Runs `fn` with `span` active, which every scoped helper does the same way.
*
* The shared no-op is never activated, so `getActiveSpan()` inside the callback
* reads null — callbacks should use the handle they're given. A pass-through
* handle is activated, so `getActiveSpan()?.traceparent()` still propagates an
* inbound trace through a service with tracing off.
*/
export function runWithActiveSpan<T>(contextManager: SpanContextManager, span: Span, fn: (span: Span) => T): T {
return span === NOOP_SPAN ? fn(span) : contextManager.with(span, () => fn(span))
}

/**
* Extracts the OTel `exception.type` / `exception.message` pair from whatever was
* thrown. Anything can be thrown in JS, so non-Errors are described by type.
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/utils/json-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,36 @@ export function toJsonSafeValue(value: unknown): unknown {

return convert(value, 0)
}

/**
* Copies caller-supplied attributes onto `target`, own enumerable keys only.
*
* Read key by key rather than spread: a getter over a disposed resource or a
* revoked proxy throws on the read itself, before the encoder's guards see it.
*/
export function assignUserAttributes<T extends Record<string, any>>(
target: T,
source: Record<string, unknown> | undefined
): T {
if (!source) {
return target
}
let keys: string[] = []
try {
keys = Object.keys(source)
} catch {
keys = []
}
for (const key of keys) {
let value: unknown
try {
value = source[key]
} catch {
value = UNSERIALIZABLE_VALUE
}
// defineProperty, not assignment: `attributes['__proto__'] = v` hits the
// prototype setter and the attribute vanishes.
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
}
return target
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/otlp-resource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assignUserAttributes } from '../traces/sanitize'
import { assignUserAttributes } from './json-utils'

/**
* Shape the logs, metrics and traces resolved configs share for resource
Expand Down
7 changes: 3 additions & 4 deletions packages/node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ import {
PostHogMetrics,
PostHogPersistedProperty,
PostHogTraces,
NOOP_SPAN,
inertSpan,
Properties,
resolveMetricsConfig,
resolveTracesConfig,
runWithActiveSpan,
RetriableOptions,
raceWithTimeout,
safeSetTimeout,
SyncSpanContextManager,
uuidv7,
} from '@posthog/core'
import type { Metrics, Span, SpanContextManager, StartSpanOptions, TraceSdkContext } from '@posthog/core'
import { resolveTracesConfig } from './traces-defaults'
import {
AllFlagsOptions,
EventMessage,
Expand Down Expand Up @@ -747,8 +747,7 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
// Tracing off: still run the callback exactly once, with an inert handle.
// A handle carrying an inbound `parent` is activated, so `getActiveSpan()`
// inside the callback can propagate the trace onward.
const span = inertSpan(options)
return span === NOOP_SPAN ? fn(span) : this._spanContextManager.with(span, () => fn(span))
return runWithActiveSpan(this._spanContextManager, inertSpan(options), fn)
}
return options ? pipeline.withSpan(name, options, fn) : pipeline.withSpan(name, fn)
}
Expand Down