autotel-posthog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +158 -0
- package/dist/index.cjs +267 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +263 -0
- package/dist/subscriber.cjs +543 -0
- package/dist/subscriber.d.cts +369 -0
- package/dist/subscriber.d.ts +369 -0
- package/dist/subscriber.js +519 -0
- package/package.json +90 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import { PostHog } from "posthog-js";
|
|
3
|
+
//#region src/posthog-like.d.ts
|
|
4
|
+
/** Exactly the members read, with PostHog's own signatures. */
|
|
5
|
+
declare global {
|
|
6
|
+
/**
|
|
7
|
+
* PostHog installs itself on the page - as the loader snippet's array stub
|
|
8
|
+
* first, then as the real instance. Declaring it here is what lets the rest
|
|
9
|
+
* of this package read `globalThis.posthog` without asserting a shape.
|
|
10
|
+
*/
|
|
11
|
+
var posthog: PostHogLike | undefined;
|
|
12
|
+
}
|
|
13
|
+
type PostHogLike = Partial<Pick<PostHog, 'get_session_id' | 'get_distinct_id' | 'get_session_replay_url' | 'getFeatureFlag' | 'sessionRecordingStarted' | 'set_config'>> & {
|
|
14
|
+
/**
|
|
15
|
+
* Pre-`sessionRecordingStarted()` fallback. Not part of the public type —
|
|
16
|
+
* PostHog exposes only `_forceAllowLocalhostNetworkCapture` on this object —
|
|
17
|
+
* so it is declared here and read defensively, never preferred.
|
|
18
|
+
*/
|
|
19
|
+
sessionRecording?: {
|
|
20
|
+
started?: boolean;
|
|
21
|
+
};
|
|
22
|
+
config?: Partial<PostHog['config']>;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/compatibility.d.ts
|
|
26
|
+
interface PostHogCompatibilityOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The PostHog instance, or a function returning it.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `globalThis.posthog`, which is where the snippet and
|
|
31
|
+
* `posthog-js` both leave it. An instance passed here is preferred — two
|
|
32
|
+
* PostHog instances on one page is a real setup and an explicit argument is a
|
|
33
|
+
* decision — but only while it can answer: hand in the loader snippet's array
|
|
34
|
+
* stub and this falls back to the global once posthog-js swaps the real
|
|
35
|
+
* library in over the top of it.
|
|
36
|
+
*/
|
|
37
|
+
posthog?: PostHogLike | (() => PostHogLike | undefined);
|
|
38
|
+
/**
|
|
39
|
+
* Flag keys to stamp on every span as `feature_flag.<key>`, so error rate and
|
|
40
|
+
* latency can be split by variant in whichever backend receives the spans.
|
|
41
|
+
*
|
|
42
|
+
* Named explicitly rather than read wholesale: every flag is another
|
|
43
|
+
* attribute on every span, and "all of them" is how an analytics convenience
|
|
44
|
+
* turns into a cardinality bill.
|
|
45
|
+
*/
|
|
46
|
+
featureFlags?: string[];
|
|
47
|
+
}
|
|
48
|
+
declare function posthogCompatibility(options?: PostHogCompatibilityOptions): SpanProcessor;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/before-send.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* The other half of the join: PostHog events that know which trace they
|
|
53
|
+
* happened inside.
|
|
54
|
+
*
|
|
55
|
+
* `posthogCompatibility()` teaches the trace about the session. This teaches
|
|
56
|
+
* the session about the trace, so a `$exception` or a funnel drop-off in
|
|
57
|
+
* PostHog carries the trace id that explains it, and the property names match
|
|
58
|
+
* the ones autotel's server-side subscriber already writes — one set of names
|
|
59
|
+
* whichever side captured the event.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* posthog.init('<key>', {
|
|
64
|
+
* before_send: [
|
|
65
|
+
* autotelBeforeSend({
|
|
66
|
+
* traceUrl: ({ traceId }) => `https://traces.example.com/${traceId}`,
|
|
67
|
+
* }),
|
|
68
|
+
* ],
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Structural copy of PostHog's `CaptureResult`. Only `properties` is touched;
|
|
74
|
+
* everything else is passed through untouched.
|
|
75
|
+
*/
|
|
76
|
+
/** What a PostHog event property can hold: JSON, since that is what is sent. */
|
|
77
|
+
type PostHogPropertyValue = string | number | boolean | null | undefined | Array<PostHogPropertyValue> | {
|
|
78
|
+
[key: string]: PostHogPropertyValue;
|
|
79
|
+
};
|
|
80
|
+
interface CaptureResultLike {
|
|
81
|
+
properties: Record<string, PostHogPropertyValue>;
|
|
82
|
+
[key: string]: PostHogPropertyValue;
|
|
83
|
+
}
|
|
84
|
+
type BeforeSendLike = (event: CaptureResultLike | null) => CaptureResultLike | null;
|
|
85
|
+
interface AutotelBeforeSendOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Build a link to the trace in your own backend.
|
|
88
|
+
*
|
|
89
|
+
* Ids correlate; they do not navigate. Whoever reads a PostHog event wants
|
|
90
|
+
* one click back to the trace, and only the app knows whether that is
|
|
91
|
+
* Traceway, Grafana, Honeycomb or a local devtools port — so the URL shape
|
|
92
|
+
* is yours to supply.
|
|
93
|
+
*
|
|
94
|
+
* Return `undefined` to add nothing for this event.
|
|
95
|
+
*/
|
|
96
|
+
traceUrl?: (context: {
|
|
97
|
+
traceId: string;
|
|
98
|
+
spanId: string;
|
|
99
|
+
}) => string | undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A PostHog `before_send` hook that adds `$trace_id` and `$span_id` from the
|
|
103
|
+
* span in progress.
|
|
104
|
+
*/
|
|
105
|
+
declare function autotelBeforeSend(options?: AutotelBeforeSendOptions): BeforeSendLike;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/join.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Wire PostHog to stamp trace context on its events, and return the span
|
|
110
|
+
* enricher for the other direction.
|
|
111
|
+
*
|
|
112
|
+
* Safe to call more than once, and safe to call on an instance that cannot be
|
|
113
|
+
* configured — the loader snippet's stub has no `set_config`, and the trace
|
|
114
|
+
* side is worth having even when the PostHog side cannot be wired.
|
|
115
|
+
*/
|
|
116
|
+
interface JoinPostHogOptions extends Omit<PostHogCompatibilityOptions, 'posthog'>, AutotelBeforeSendOptions {}
|
|
117
|
+
declare function joinPostHog(posthog: PostHogLike | (() => PostHogLike | undefined), options?: JoinPostHogOptions): SpanProcessor;
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/session-id.d.ts
|
|
120
|
+
declare function posthogSessionId(posthog?: PostHogLike): string | undefined;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { type AutotelBeforeSendOptions, type BeforeSendLike, type CaptureResultLike, type JoinPostHogOptions, type PostHogCompatibilityOptions, type PostHogLike, autotelBeforeSend, joinPostHog, posthogCompatibility, posthogSessionId };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import { PostHog } from "posthog-js";
|
|
3
|
+
//#region src/posthog-like.d.ts
|
|
4
|
+
/** Exactly the members read, with PostHog's own signatures. */
|
|
5
|
+
declare global {
|
|
6
|
+
/**
|
|
7
|
+
* PostHog installs itself on the page - as the loader snippet's array stub
|
|
8
|
+
* first, then as the real instance. Declaring it here is what lets the rest
|
|
9
|
+
* of this package read `globalThis.posthog` without asserting a shape.
|
|
10
|
+
*/
|
|
11
|
+
var posthog: PostHogLike | undefined;
|
|
12
|
+
}
|
|
13
|
+
type PostHogLike = Partial<Pick<PostHog, 'get_session_id' | 'get_distinct_id' | 'get_session_replay_url' | 'getFeatureFlag' | 'sessionRecordingStarted' | 'set_config'>> & {
|
|
14
|
+
/**
|
|
15
|
+
* Pre-`sessionRecordingStarted()` fallback. Not part of the public type —
|
|
16
|
+
* PostHog exposes only `_forceAllowLocalhostNetworkCapture` on this object —
|
|
17
|
+
* so it is declared here and read defensively, never preferred.
|
|
18
|
+
*/
|
|
19
|
+
sessionRecording?: {
|
|
20
|
+
started?: boolean;
|
|
21
|
+
};
|
|
22
|
+
config?: Partial<PostHog['config']>;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/compatibility.d.ts
|
|
26
|
+
interface PostHogCompatibilityOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The PostHog instance, or a function returning it.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `globalThis.posthog`, which is where the snippet and
|
|
31
|
+
* `posthog-js` both leave it. An instance passed here is preferred — two
|
|
32
|
+
* PostHog instances on one page is a real setup and an explicit argument is a
|
|
33
|
+
* decision — but only while it can answer: hand in the loader snippet's array
|
|
34
|
+
* stub and this falls back to the global once posthog-js swaps the real
|
|
35
|
+
* library in over the top of it.
|
|
36
|
+
*/
|
|
37
|
+
posthog?: PostHogLike | (() => PostHogLike | undefined);
|
|
38
|
+
/**
|
|
39
|
+
* Flag keys to stamp on every span as `feature_flag.<key>`, so error rate and
|
|
40
|
+
* latency can be split by variant in whichever backend receives the spans.
|
|
41
|
+
*
|
|
42
|
+
* Named explicitly rather than read wholesale: every flag is another
|
|
43
|
+
* attribute on every span, and "all of them" is how an analytics convenience
|
|
44
|
+
* turns into a cardinality bill.
|
|
45
|
+
*/
|
|
46
|
+
featureFlags?: string[];
|
|
47
|
+
}
|
|
48
|
+
declare function posthogCompatibility(options?: PostHogCompatibilityOptions): SpanProcessor;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/before-send.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* The other half of the join: PostHog events that know which trace they
|
|
53
|
+
* happened inside.
|
|
54
|
+
*
|
|
55
|
+
* `posthogCompatibility()` teaches the trace about the session. This teaches
|
|
56
|
+
* the session about the trace, so a `$exception` or a funnel drop-off in
|
|
57
|
+
* PostHog carries the trace id that explains it, and the property names match
|
|
58
|
+
* the ones autotel's server-side subscriber already writes — one set of names
|
|
59
|
+
* whichever side captured the event.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* posthog.init('<key>', {
|
|
64
|
+
* before_send: [
|
|
65
|
+
* autotelBeforeSend({
|
|
66
|
+
* traceUrl: ({ traceId }) => `https://traces.example.com/${traceId}`,
|
|
67
|
+
* }),
|
|
68
|
+
* ],
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Structural copy of PostHog's `CaptureResult`. Only `properties` is touched;
|
|
74
|
+
* everything else is passed through untouched.
|
|
75
|
+
*/
|
|
76
|
+
/** What a PostHog event property can hold: JSON, since that is what is sent. */
|
|
77
|
+
type PostHogPropertyValue = string | number | boolean | null | undefined | Array<PostHogPropertyValue> | {
|
|
78
|
+
[key: string]: PostHogPropertyValue;
|
|
79
|
+
};
|
|
80
|
+
interface CaptureResultLike {
|
|
81
|
+
properties: Record<string, PostHogPropertyValue>;
|
|
82
|
+
[key: string]: PostHogPropertyValue;
|
|
83
|
+
}
|
|
84
|
+
type BeforeSendLike = (event: CaptureResultLike | null) => CaptureResultLike | null;
|
|
85
|
+
interface AutotelBeforeSendOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Build a link to the trace in your own backend.
|
|
88
|
+
*
|
|
89
|
+
* Ids correlate; they do not navigate. Whoever reads a PostHog event wants
|
|
90
|
+
* one click back to the trace, and only the app knows whether that is
|
|
91
|
+
* Traceway, Grafana, Honeycomb or a local devtools port — so the URL shape
|
|
92
|
+
* is yours to supply.
|
|
93
|
+
*
|
|
94
|
+
* Return `undefined` to add nothing for this event.
|
|
95
|
+
*/
|
|
96
|
+
traceUrl?: (context: {
|
|
97
|
+
traceId: string;
|
|
98
|
+
spanId: string;
|
|
99
|
+
}) => string | undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A PostHog `before_send` hook that adds `$trace_id` and `$span_id` from the
|
|
103
|
+
* span in progress.
|
|
104
|
+
*/
|
|
105
|
+
declare function autotelBeforeSend(options?: AutotelBeforeSendOptions): BeforeSendLike;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/join.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Wire PostHog to stamp trace context on its events, and return the span
|
|
110
|
+
* enricher for the other direction.
|
|
111
|
+
*
|
|
112
|
+
* Safe to call more than once, and safe to call on an instance that cannot be
|
|
113
|
+
* configured — the loader snippet's stub has no `set_config`, and the trace
|
|
114
|
+
* side is worth having even when the PostHog side cannot be wired.
|
|
115
|
+
*/
|
|
116
|
+
interface JoinPostHogOptions extends Omit<PostHogCompatibilityOptions, 'posthog'>, AutotelBeforeSendOptions {}
|
|
117
|
+
declare function joinPostHog(posthog: PostHogLike | (() => PostHogLike | undefined), options?: JoinPostHogOptions): SpanProcessor;
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/session-id.d.ts
|
|
120
|
+
declare function posthogSessionId(posthog?: PostHogLike): string | undefined;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { type AutotelBeforeSendOptions, type BeforeSendLike, type CaptureResultLike, type JoinPostHogOptions, type PostHogCompatibilityOptions, type PostHogLike, autotelBeforeSend, joinPostHog, posthogCompatibility, posthogSessionId };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { context, trace } from "@opentelemetry/api";
|
|
2
|
+
//#region src/posthog-like.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether this object can actually answer questions yet.
|
|
5
|
+
*
|
|
6
|
+
* The loader snippet leaves an array on `window.posthog` that queues calls, and
|
|
7
|
+
* posthog-js later *replaces* it with the real instance. An integration holding
|
|
8
|
+
* the array from before the swap holds something that will never answer, so the
|
|
9
|
+
* only useful test is whether the methods are there.
|
|
10
|
+
*/
|
|
11
|
+
function isUsable(posthog) {
|
|
12
|
+
return typeof posthog?.get_session_id === "function";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Call a PostHog getter without letting the page's analytics break the page's
|
|
16
|
+
* tracing. A stub throws `TypeError`, a partly-initialized instance returns an
|
|
17
|
+
* empty string, and neither is worth an attribute.
|
|
18
|
+
*/
|
|
19
|
+
function readString(fn, receiver) {
|
|
20
|
+
if (typeof fn !== "function") return void 0;
|
|
21
|
+
try {
|
|
22
|
+
const value = fn.call(receiver);
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readSessionId(posthog) {
|
|
29
|
+
return readString(posthog.get_session_id, posthog);
|
|
30
|
+
}
|
|
31
|
+
function readDistinctId(posthog) {
|
|
32
|
+
return readString(posthog.get_distinct_id, posthog);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a replay actually exists to link to.
|
|
36
|
+
*
|
|
37
|
+
* `get_session_replay_url()` composes a URL out of the session id whether or
|
|
38
|
+
* not anything was recorded — replay disabled, sampled out, or simply not
|
|
39
|
+
* started yet all still produce a link, and it lands on an empty player. Only
|
|
40
|
+
* an affirmative answer counts, so an instance too old or too stubbed to say
|
|
41
|
+
* produces no link rather than a confident wrong one.
|
|
42
|
+
*/
|
|
43
|
+
function isRecording(posthog) {
|
|
44
|
+
try {
|
|
45
|
+
if (typeof posthog.sessionRecordingStarted === "function") return posthog.sessionRecordingStarted() === true;
|
|
46
|
+
return posthog.sessionRecording?.started === true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readReplayUrl(posthog) {
|
|
52
|
+
if (!isRecording(posthog)) return void 0;
|
|
53
|
+
const fn = posthog.get_session_replay_url;
|
|
54
|
+
if (typeof fn !== "function") return void 0;
|
|
55
|
+
try {
|
|
56
|
+
const value = fn.call(posthog, { withTimestamp: true });
|
|
57
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* An evaluated flag value.
|
|
64
|
+
*
|
|
65
|
+
* `false` is an answer — "this person is not in the variant" — and is kept as a
|
|
66
|
+
* boolean so a query can ask for it. Only `undefined`, meaning PostHog has no
|
|
67
|
+
* opinion yet, is dropped: an attribute that says nothing is worse than an
|
|
68
|
+
* absent one.
|
|
69
|
+
*/
|
|
70
|
+
function readFeatureFlag(posthog, key) {
|
|
71
|
+
const fn = posthog.getFeatureFlag;
|
|
72
|
+
if (typeof fn !== "function") return void 0;
|
|
73
|
+
try {
|
|
74
|
+
const value = fn.call(posthog, key);
|
|
75
|
+
if (value === void 0) return void 0;
|
|
76
|
+
return typeof value === "boolean" ? value : String(value);
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/compatibility.ts
|
|
83
|
+
/** `SpanStatusCode.ERROR`, without importing the enum for one comparison. */
|
|
84
|
+
const STATUS_ERROR = 2;
|
|
85
|
+
function failed(span) {
|
|
86
|
+
return span.status?.code === STATUS_ERROR || span.attributes["exception.type"] !== void 0 || span.events?.some((event) => event.name === "exception") === true;
|
|
87
|
+
}
|
|
88
|
+
function resolvePostHog(options) {
|
|
89
|
+
const configured = typeof options.posthog === "function" ? options.posthog() : options.posthog;
|
|
90
|
+
if (isUsable(configured)) return configured;
|
|
91
|
+
const global = globalThis.posthog;
|
|
92
|
+
return isUsable(global) ? global : void 0;
|
|
93
|
+
}
|
|
94
|
+
function posthogCompatibility(options = {}) {
|
|
95
|
+
return {
|
|
96
|
+
/**
|
|
97
|
+
* Identity is read here, not in `onEnd`, because it is a fact about when
|
|
98
|
+
* the operation happened. PostHog rotates a session after 30 minutes idle,
|
|
99
|
+
* and `identify()` can land mid-request; a long span asking at the end
|
|
100
|
+
* would be filed under whoever the visitor had become by then rather than
|
|
101
|
+
* who started it.
|
|
102
|
+
*/
|
|
103
|
+
onStart(span, _context) {
|
|
104
|
+
const posthog = resolvePostHog(options);
|
|
105
|
+
if (!posthog) return;
|
|
106
|
+
const sessionId = readSessionId(posthog);
|
|
107
|
+
if (sessionId !== void 0) span.setAttribute("session.id", sessionId);
|
|
108
|
+
const attributes = span.attributes;
|
|
109
|
+
const fill = (key, value) => {
|
|
110
|
+
if (value !== void 0 && attributes[key] === void 0) span.setAttribute(key, value);
|
|
111
|
+
};
|
|
112
|
+
fill("user.id", readDistinctId(posthog));
|
|
113
|
+
for (const key of options.featureFlags ?? []) fill(`feature_flag.${key}`, readFeatureFlag(posthog, key));
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Only the replay link is left for the end, because only the end knows
|
|
117
|
+
* whether the span failed.
|
|
118
|
+
*/
|
|
119
|
+
onEnd(span) {
|
|
120
|
+
if (!failed(span)) return;
|
|
121
|
+
const posthog = resolvePostHog(options);
|
|
122
|
+
if (!posthog) return;
|
|
123
|
+
const attributes = span.attributes;
|
|
124
|
+
if (attributes["session.replay.url"] !== void 0) return;
|
|
125
|
+
const current = readSessionId(posthog);
|
|
126
|
+
if (current === void 0 || current !== attributes["session.id"]) return;
|
|
127
|
+
const url = readReplayUrl(posthog);
|
|
128
|
+
if (url !== void 0) attributes["session.replay.url"] = url;
|
|
129
|
+
},
|
|
130
|
+
forceFlush() {
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
},
|
|
133
|
+
shutdown() {
|
|
134
|
+
return Promise.resolve();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/before-send.ts
|
|
140
|
+
/**
|
|
141
|
+
* The other half of the join: PostHog events that know which trace they
|
|
142
|
+
* happened inside.
|
|
143
|
+
*
|
|
144
|
+
* `posthogCompatibility()` teaches the trace about the session. This teaches
|
|
145
|
+
* the session about the trace, so a `$exception` or a funnel drop-off in
|
|
146
|
+
* PostHog carries the trace id that explains it, and the property names match
|
|
147
|
+
* the ones autotel's server-side subscriber already writes — one set of names
|
|
148
|
+
* whichever side captured the event.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* posthog.init('<key>', {
|
|
153
|
+
* before_send: [
|
|
154
|
+
* autotelBeforeSend({
|
|
155
|
+
* traceUrl: ({ traceId }) => `https://traces.example.com/${traceId}`,
|
|
156
|
+
* }),
|
|
157
|
+
* ],
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* A PostHog `before_send` hook that adds `$trace_id` and `$span_id` from the
|
|
163
|
+
* span in progress.
|
|
164
|
+
*/
|
|
165
|
+
function autotelBeforeSend(options = {}) {
|
|
166
|
+
return (event) => {
|
|
167
|
+
if (event === null) return null;
|
|
168
|
+
const spanContext = trace.getSpanContext(context.active());
|
|
169
|
+
if (!spanContext) return event;
|
|
170
|
+
const properties = event.properties;
|
|
171
|
+
if (properties["$trace_id"] === void 0) properties["$trace_id"] = spanContext.traceId;
|
|
172
|
+
if (properties["$span_id"] === void 0) properties["$span_id"] = spanContext.spanId;
|
|
173
|
+
if (options.traceUrl && properties["$trace_url"] === void 0) try {
|
|
174
|
+
const url = options.traceUrl({
|
|
175
|
+
traceId: spanContext.traceId,
|
|
176
|
+
spanId: spanContext.spanId
|
|
177
|
+
});
|
|
178
|
+
if (url !== void 0) properties["$trace_url"] = url;
|
|
179
|
+
} catch {}
|
|
180
|
+
return event;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/join.ts
|
|
185
|
+
/**
|
|
186
|
+
* Marks our hook so a second call recognises it. Framework code runs more than
|
|
187
|
+
* once — strict mode, HMR, a re-render — and a chain that grows on every render
|
|
188
|
+
* stamps the same properties again and again.
|
|
189
|
+
*/
|
|
190
|
+
const MARKER = "__autotelBeforeSend";
|
|
191
|
+
function existingHooks(posthog) {
|
|
192
|
+
const current = posthog.config?.before_send;
|
|
193
|
+
if (Array.isArray(current)) return current;
|
|
194
|
+
return current ? [current] : [];
|
|
195
|
+
}
|
|
196
|
+
function joinPostHog(posthog, options = {}) {
|
|
197
|
+
const resolve = () => resolvePostHog({
|
|
198
|
+
...options,
|
|
199
|
+
posthog
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Returns true once the hook is in place. Kept retryable because the loader
|
|
203
|
+
* snippet's stub has no `set_config`: giving up at call time would leave
|
|
204
|
+
* every PostHog event on that page without its trace for the life of the
|
|
205
|
+
* page, and the real library usually arrives a moment later.
|
|
206
|
+
*/
|
|
207
|
+
const wire = () => {
|
|
208
|
+
try {
|
|
209
|
+
const instance = resolve();
|
|
210
|
+
if (!instance) return false;
|
|
211
|
+
const hooks = existingHooks(instance);
|
|
212
|
+
if (hooks.some((hook) => hook[MARKER])) return true;
|
|
213
|
+
if (typeof instance.set_config !== "function") return false;
|
|
214
|
+
const hook = autotelBeforeSend(options);
|
|
215
|
+
hook[MARKER] = true;
|
|
216
|
+
instance.set_config({ before_send: [...hooks, hook] });
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
let wired = wire();
|
|
223
|
+
const enricher = posthogCompatibility({
|
|
224
|
+
...options,
|
|
225
|
+
posthog
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
onStart(span, context) {
|
|
229
|
+
if (!wired) wired = wire();
|
|
230
|
+
enricher.onStart(span, context);
|
|
231
|
+
},
|
|
232
|
+
onEnd: (span) => enricher.onEnd(span),
|
|
233
|
+
forceFlush: () => enricher.forceFlush(),
|
|
234
|
+
shutdown: () => enricher.shutdown()
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/session-id.ts
|
|
239
|
+
/**
|
|
240
|
+
* PostHog's session id, shaped for `autotel-web`'s `session.id` provider.
|
|
241
|
+
*
|
|
242
|
+
* The span enricher covers full mode, where there is an OpenTelemetry pipeline
|
|
243
|
+
* to hang a processor on. The minimal browser build has no such pipeline — it
|
|
244
|
+
* writes spans straight to OTLP — so it takes the id from a function instead,
|
|
245
|
+
* and this is that function:
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* import { init } from 'autotel-web';
|
|
249
|
+
* import { posthogSessionId } from 'autotel-posthog';
|
|
250
|
+
*
|
|
251
|
+
* init({ service: 'web', session: { id: posthogSessionId } });
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* Returns `undefined` when PostHog is absent or not yet initialized, which is
|
|
255
|
+
* the signal for autotel-web to fall back to minting its own id rather than
|
|
256
|
+
* emitting spans with no session at all.
|
|
257
|
+
*/
|
|
258
|
+
function posthogSessionId(posthog) {
|
|
259
|
+
const instance = posthog ?? globalThis.posthog ?? void 0;
|
|
260
|
+
return instance ? readSessionId(instance) : void 0;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
export { autotelBeforeSend, joinPostHog, posthogCompatibility, posthogSessionId };
|