@threadplane/telemetry 0.0.46
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/README.md +143 -0
- package/browser/README.md +143 -0
- package/browser/fesm2022/threadplane-telemetry.mjs +168 -0
- package/browser/fesm2022/threadplane-telemetry.mjs.map +1 -0
- package/browser/index.d.ts +1 -0
- package/browser/types/threadplane-telemetry.d.ts +74 -0
- package/browser/types/threadplane-telemetry.d.ts.map +1 -0
- package/index.d.ts +6 -0
- package/index.d.ts.map +1 -0
- package/index.js +4 -0
- package/node/adapter.d.ts +26 -0
- package/node/adapter.d.ts.map +1 -0
- package/node/adapter.js +30 -0
- package/node/client.d.ts +16 -0
- package/node/client.d.ts.map +1 -0
- package/node/client.js +118 -0
- package/node/disable.d.ts +4 -0
- package/node/disable.d.ts.map +1 -0
- package/node/disable.js +11 -0
- package/node/index.d.ts +6 -0
- package/node/index.d.ts.map +1 -0
- package/node/index.js +3 -0
- package/node/postinstall.d.ts +13 -0
- package/node/postinstall.d.ts.map +1 -0
- package/node/postinstall.js +110 -0
- package/package.json +60 -0
- package/shared/anon-id.d.ts +3 -0
- package/shared/anon-id.d.ts.map +1 -0
- package/shared/anon-id.js +11 -0
- package/shared/env.d.ts +5 -0
- package/shared/env.d.ts.map +1 -0
- package/shared/env.js +22 -0
- package/shared/events.d.ts +4 -0
- package/shared/events.d.ts.map +1 -0
- package/shared/events.js +1 -0
- package/shared/hash.d.ts +2 -0
- package/shared/hash.d.ts.map +1 -0
- package/shared/hash.js +7 -0
- package/shared/personal-email-domains.d.ts +3 -0
- package/shared/personal-email-domains.d.ts.map +1 -0
- package/shared/personal-email-domains.js +27 -0
- package/shared/properties.d.ts +5 -0
- package/shared/properties.d.ts.map +1 -0
- package/shared/properties.js +40 -0
- package/shared/public-api.d.ts +4 -0
- package/shared/public-api.d.ts.map +1 -0
- package/shared/public-api.js +2 -0
- package/shared/sample.d.ts +2 -0
- package/shared/sample.d.ts.map +1 -0
- package/shared/sample.js +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @threadplane/telemetry
|
|
2
|
+
|
|
3
|
+
This README is the public trust contract for `@threadplane/*` telemetry. It is linked
|
|
4
|
+
from package install notices and should stay aligned with implementation.
|
|
5
|
+
|
|
6
|
+
## Imports
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Browser (Angular DI provider)
|
|
10
|
+
import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser';
|
|
11
|
+
|
|
12
|
+
// Node (server adapters)
|
|
13
|
+
import {
|
|
14
|
+
captureRuntimeInstanceCreated,
|
|
15
|
+
captureRuntimeRequestCreated,
|
|
16
|
+
captureStreamStarted,
|
|
17
|
+
captureStreamEnded,
|
|
18
|
+
captureStreamErrored,
|
|
19
|
+
disableTelemetry,
|
|
20
|
+
} from '@threadplane/telemetry/node';
|
|
21
|
+
|
|
22
|
+
// Shared utilities (events, env detection, hashing)
|
|
23
|
+
import { isTelemetryDisabled, sha256, getAnonId } from '@threadplane/telemetry';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What this package is
|
|
27
|
+
|
|
28
|
+
The single telemetry surface for `@threadplane/*`. It exists so we can answer "how is Threadplane being used?" without instrumenting browser packages that ship to end-users.
|
|
29
|
+
|
|
30
|
+
## What is and isn't telemetered
|
|
31
|
+
|
|
32
|
+
**Telemetered by default (Node, opt-out):**
|
|
33
|
+
- `ngaf:postinstall` — fires once per dependency/global install of a published `@threadplane/*` package. Properties: package name, package version, Node version, OS, CPU architecture, package manager name/version, installer-reported Node/OS/architecture, workspace/global install flags when npm exposes them, sample weight. It uses a per-process anonymous id. No project path, no raw environment variables, no dependency tree, no installer IP address.
|
|
34
|
+
- `ngaf:runtime_instance_created` — server adapters (LangGraph, AG-UI) call this when they spin up. Properties: which transport, which model provider (string), Angular peer version. **No API keys**, no endpoint hostnames, no user data.
|
|
35
|
+
- `ngaf:runtime_request_created` — server adapters call this when they create a transport request. Properties: transport, request type, provider, model. No prompts, thread IDs, assistant IDs, endpoint URLs, or headers.
|
|
36
|
+
- `ngaf:stream_started` / `ngaf:stream_ended` / `ngaf:stream_errored` — per-request lifecycle on server adapters. Properties: provider, model name, duration, error class. No prompts, no completions, no message content.
|
|
37
|
+
|
|
38
|
+
**Telemetered only on explicit opt-in (Browser):**
|
|
39
|
+
- Nothing fires unless the consumer calls `provideThreadplaneTelemetry({ enabled: true, sink })` or `provideThreadplaneTelemetry({ enabled: true, endpoint })` in their root providers.
|
|
40
|
+
- When opted in: `ngaf:browser_provided`, `ngaf:browser_chat_init`, and browser-side runtime lifecycle events explicitly captured by the app (`ngaf:runtime_instance_created`, `ngaf:runtime_request_created`, `ngaf:stream_started`, `ngaf:stream_ended`, `ngaf:stream_errored`). Anonymous, no message content.
|
|
41
|
+
|
|
42
|
+
**Never telemetered (by anyone, at any time):**
|
|
43
|
+
- Message content (user prompts, model completions, tool call inputs/outputs).
|
|
44
|
+
- Personally identifiable information beyond `email_domain` on explicit server conversion events on the website.
|
|
45
|
+
- API keys, vendor credentials, project paths, environment variables.
|
|
46
|
+
|
|
47
|
+
## Opt-out
|
|
48
|
+
|
|
49
|
+
Node telemetry is on by default. Three ways to opt out — any one turns it off.
|
|
50
|
+
|
|
51
|
+
| Method | How |
|
|
52
|
+
|--------|-----|
|
|
53
|
+
| Cross-vendor env var | `DO_NOT_TRACK=1` or `DO_NOT_TRACK=true` |
|
|
54
|
+
| npm config env var | `npm_config_do_not_track=true` |
|
|
55
|
+
| Package env var | `NGAF_TELEMETRY_DISABLED=1` or `NGAF_TELEMETRY_DISABLED=true` |
|
|
56
|
+
| Programmatic | `import { disableTelemetry } from '@threadplane/telemetry/node'; disableTelemetry();` before any other `@threadplane/*` import |
|
|
57
|
+
|
|
58
|
+
CI environments (`CI=true`, `GITHUB_ACTIONS=true`, etc.) are auto-detected and treated as opt-out by default.
|
|
59
|
+
|
|
60
|
+
Local top-level installs are skipped by default. Dependency installs and global installs are eligible unless opted out.
|
|
61
|
+
|
|
62
|
+
The postinstall script prints a single line on stdout only when the install ping was actually accepted by the ingest endpoint. The line is suppressed in CI.
|
|
63
|
+
|
|
64
|
+
To inspect the install payload locally, run with `DEBUG=ngaf:telemetry`.
|
|
65
|
+
|
|
66
|
+
## Opt-in (browser)
|
|
67
|
+
|
|
68
|
+
Browser telemetry is **off by default** and never fires from the library itself. To enable in your Angular app:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// app.config.ts (or wherever you bootstrap)
|
|
72
|
+
import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser';
|
|
73
|
+
|
|
74
|
+
export const appConfig: ApplicationConfig = {
|
|
75
|
+
providers: [
|
|
76
|
+
// ...
|
|
77
|
+
provideThreadplaneTelemetry({
|
|
78
|
+
enabled: true,
|
|
79
|
+
endpoint: '/api/telemetry',
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The endpoint receives neutral JSON:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"event": "ngaf:stream_started",
|
|
90
|
+
"distinctId": "browser:<ephemeral-id>",
|
|
91
|
+
"properties": {
|
|
92
|
+
"surface": "my_app",
|
|
93
|
+
"sample_weight": 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
You can also pass `sink: async ({ event, properties }) => { ... }` and route events through your own analytics client. Legacy `posthogKey` / `posthogHost` options still work for existing adopters, but new app code should prefer `sink` or `endpoint` so the public API is vendor-neutral.
|
|
99
|
+
|
|
100
|
+
If you don't call `provideThreadplaneTelemetry({ enabled: true })`, every telemetry helper in `@threadplane/*` browser packages no-ops. No network calls, ever.
|
|
101
|
+
|
|
102
|
+
## Sampling
|
|
103
|
+
|
|
104
|
+
- Default sample rate: **1.0** (100%) at current scale.
|
|
105
|
+
- Configurable via `NGAF_TELEMETRY_SAMPLE_RATE` env var (Node).
|
|
106
|
+
- Every event carries a `sample_weight` property so future de-sampling at query time works correctly.
|
|
107
|
+
|
|
108
|
+
## Anonymous id strategy
|
|
109
|
+
|
|
110
|
+
- Per-process UUID (`anon_<uuid>`), regenerated every Node process boot.
|
|
111
|
+
- No persistence across restarts. No persistent identifier.
|
|
112
|
+
- Browser opt-in endpoint delivery uses an ephemeral per-service-instance id (`browser:<uuid>`). It is not written to localStorage or cookies.
|
|
113
|
+
|
|
114
|
+
## Self-hosting
|
|
115
|
+
|
|
116
|
+
Enterprise users can redirect Node telemetry to their own ingest:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
NGAF_TELEMETRY_INGEST_URL=https://telemetry.acme-internal.example.com/api/ingest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Default ingest (when env var is unset) is a thin proxy on the Threadplane website (`https://threadplane.ai/api/ingest`) that accepts the `@threadplane/telemetry` JSON payload and forwards `ngaf:*` events to our PostHog project without forwarding installer IP addresses. Source of the proxy lives in `apps/website/src/app/api/ingest/`.
|
|
123
|
+
|
|
124
|
+
## Verifying telemetry is silent
|
|
125
|
+
|
|
126
|
+
The `@threadplane/telemetry/browser` unit test suite includes a permanent trust test:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
test('no network call occurs when provideThreadplaneTelemetry is never called', ...)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If this test ever fails, the trust contract has been violated and the build blocks.
|
|
133
|
+
|
|
134
|
+
## What's intentionally not in this package
|
|
135
|
+
|
|
136
|
+
- Session replay. (Not in Phase 0–1.)
|
|
137
|
+
- Cross-session identity stitching.
|
|
138
|
+
- Heuristic PII detection. (Redaction is explicit and config-driven only.)
|
|
139
|
+
- Default browser writes to anyone's PostHog instance — including ours — without explicit configuration.
|
|
140
|
+
|
|
141
|
+
## Reporting an issue
|
|
142
|
+
|
|
143
|
+
If you observe telemetry that you believe contradicts this contract, please open an issue at https://github.com/cacheplane/angular-agent-framework/issues — security-tagged. We treat it as a P0.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @threadplane/telemetry
|
|
2
|
+
|
|
3
|
+
This README is the public trust contract for `@threadplane/*` telemetry. It is linked
|
|
4
|
+
from package install notices and should stay aligned with implementation.
|
|
5
|
+
|
|
6
|
+
## Imports
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Browser (Angular DI provider)
|
|
10
|
+
import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser';
|
|
11
|
+
|
|
12
|
+
// Node (server adapters)
|
|
13
|
+
import {
|
|
14
|
+
captureRuntimeInstanceCreated,
|
|
15
|
+
captureRuntimeRequestCreated,
|
|
16
|
+
captureStreamStarted,
|
|
17
|
+
captureStreamEnded,
|
|
18
|
+
captureStreamErrored,
|
|
19
|
+
disableTelemetry,
|
|
20
|
+
} from '@threadplane/telemetry/node';
|
|
21
|
+
|
|
22
|
+
// Shared utilities (events, env detection, hashing)
|
|
23
|
+
import { isTelemetryDisabled, sha256, getAnonId } from '@threadplane/telemetry';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What this package is
|
|
27
|
+
|
|
28
|
+
The single telemetry surface for `@threadplane/*`. It exists so we can answer "how is Threadplane being used?" without instrumenting browser packages that ship to end-users.
|
|
29
|
+
|
|
30
|
+
## What is and isn't telemetered
|
|
31
|
+
|
|
32
|
+
**Telemetered by default (Node, opt-out):**
|
|
33
|
+
- `ngaf:postinstall` — fires once per dependency/global install of a published `@threadplane/*` package. Properties: package name, package version, Node version, OS, CPU architecture, package manager name/version, installer-reported Node/OS/architecture, workspace/global install flags when npm exposes them, sample weight. It uses a per-process anonymous id. No project path, no raw environment variables, no dependency tree, no installer IP address.
|
|
34
|
+
- `ngaf:runtime_instance_created` — server adapters (LangGraph, AG-UI) call this when they spin up. Properties: which transport, which model provider (string), Angular peer version. **No API keys**, no endpoint hostnames, no user data.
|
|
35
|
+
- `ngaf:runtime_request_created` — server adapters call this when they create a transport request. Properties: transport, request type, provider, model. No prompts, thread IDs, assistant IDs, endpoint URLs, or headers.
|
|
36
|
+
- `ngaf:stream_started` / `ngaf:stream_ended` / `ngaf:stream_errored` — per-request lifecycle on server adapters. Properties: provider, model name, duration, error class. No prompts, no completions, no message content.
|
|
37
|
+
|
|
38
|
+
**Telemetered only on explicit opt-in (Browser):**
|
|
39
|
+
- Nothing fires unless the consumer calls `provideThreadplaneTelemetry({ enabled: true, sink })` or `provideThreadplaneTelemetry({ enabled: true, endpoint })` in their root providers.
|
|
40
|
+
- When opted in: `ngaf:browser_provided`, `ngaf:browser_chat_init`, and browser-side runtime lifecycle events explicitly captured by the app (`ngaf:runtime_instance_created`, `ngaf:runtime_request_created`, `ngaf:stream_started`, `ngaf:stream_ended`, `ngaf:stream_errored`). Anonymous, no message content.
|
|
41
|
+
|
|
42
|
+
**Never telemetered (by anyone, at any time):**
|
|
43
|
+
- Message content (user prompts, model completions, tool call inputs/outputs).
|
|
44
|
+
- Personally identifiable information beyond `email_domain` on explicit server conversion events on the website.
|
|
45
|
+
- API keys, vendor credentials, project paths, environment variables.
|
|
46
|
+
|
|
47
|
+
## Opt-out
|
|
48
|
+
|
|
49
|
+
Node telemetry is on by default. Three ways to opt out — any one turns it off.
|
|
50
|
+
|
|
51
|
+
| Method | How |
|
|
52
|
+
|--------|-----|
|
|
53
|
+
| Cross-vendor env var | `DO_NOT_TRACK=1` or `DO_NOT_TRACK=true` |
|
|
54
|
+
| npm config env var | `npm_config_do_not_track=true` |
|
|
55
|
+
| Package env var | `NGAF_TELEMETRY_DISABLED=1` or `NGAF_TELEMETRY_DISABLED=true` |
|
|
56
|
+
| Programmatic | `import { disableTelemetry } from '@threadplane/telemetry/node'; disableTelemetry();` before any other `@threadplane/*` import |
|
|
57
|
+
|
|
58
|
+
CI environments (`CI=true`, `GITHUB_ACTIONS=true`, etc.) are auto-detected and treated as opt-out by default.
|
|
59
|
+
|
|
60
|
+
Local top-level installs are skipped by default. Dependency installs and global installs are eligible unless opted out.
|
|
61
|
+
|
|
62
|
+
The postinstall script prints a single line on stdout only when the install ping was actually accepted by the ingest endpoint. The line is suppressed in CI.
|
|
63
|
+
|
|
64
|
+
To inspect the install payload locally, run with `DEBUG=ngaf:telemetry`.
|
|
65
|
+
|
|
66
|
+
## Opt-in (browser)
|
|
67
|
+
|
|
68
|
+
Browser telemetry is **off by default** and never fires from the library itself. To enable in your Angular app:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// app.config.ts (or wherever you bootstrap)
|
|
72
|
+
import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser';
|
|
73
|
+
|
|
74
|
+
export const appConfig: ApplicationConfig = {
|
|
75
|
+
providers: [
|
|
76
|
+
// ...
|
|
77
|
+
provideThreadplaneTelemetry({
|
|
78
|
+
enabled: true,
|
|
79
|
+
endpoint: '/api/telemetry',
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The endpoint receives neutral JSON:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"event": "ngaf:stream_started",
|
|
90
|
+
"distinctId": "browser:<ephemeral-id>",
|
|
91
|
+
"properties": {
|
|
92
|
+
"surface": "my_app",
|
|
93
|
+
"sample_weight": 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
You can also pass `sink: async ({ event, properties }) => { ... }` and route events through your own analytics client. Legacy `posthogKey` / `posthogHost` options still work for existing adopters, but new app code should prefer `sink` or `endpoint` so the public API is vendor-neutral.
|
|
99
|
+
|
|
100
|
+
If you don't call `provideThreadplaneTelemetry({ enabled: true })`, every telemetry helper in `@threadplane/*` browser packages no-ops. No network calls, ever.
|
|
101
|
+
|
|
102
|
+
## Sampling
|
|
103
|
+
|
|
104
|
+
- Default sample rate: **1.0** (100%) at current scale.
|
|
105
|
+
- Configurable via `NGAF_TELEMETRY_SAMPLE_RATE` env var (Node).
|
|
106
|
+
- Every event carries a `sample_weight` property so future de-sampling at query time works correctly.
|
|
107
|
+
|
|
108
|
+
## Anonymous id strategy
|
|
109
|
+
|
|
110
|
+
- Per-process UUID (`anon_<uuid>`), regenerated every Node process boot.
|
|
111
|
+
- No persistence across restarts. No persistent identifier.
|
|
112
|
+
- Browser opt-in endpoint delivery uses an ephemeral per-service-instance id (`browser:<uuid>`). It is not written to localStorage or cookies.
|
|
113
|
+
|
|
114
|
+
## Self-hosting
|
|
115
|
+
|
|
116
|
+
Enterprise users can redirect Node telemetry to their own ingest:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
NGAF_TELEMETRY_INGEST_URL=https://telemetry.acme-internal.example.com/api/ingest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Default ingest (when env var is unset) is a thin proxy on the Threadplane website (`https://threadplane.ai/api/ingest`) that accepts the `@threadplane/telemetry` JSON payload and forwards `ngaf:*` events to our PostHog project without forwarding installer IP addresses. Source of the proxy lives in `apps/website/src/app/api/ingest/`.
|
|
123
|
+
|
|
124
|
+
## Verifying telemetry is silent
|
|
125
|
+
|
|
126
|
+
The `@threadplane/telemetry/browser` unit test suite includes a permanent trust test:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
test('no network call occurs when provideThreadplaneTelemetry is never called', ...)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If this test ever fails, the trust contract has been violated and the build blocks.
|
|
133
|
+
|
|
134
|
+
## What's intentionally not in this package
|
|
135
|
+
|
|
136
|
+
- Session replay. (Not in Phase 0–1.)
|
|
137
|
+
- Cross-session identity stitching.
|
|
138
|
+
- Heuristic PII detection. (Redaction is explicit and config-driven only.)
|
|
139
|
+
- Default browser writes to anyone's PostHog instance — including ours — without explicit configuration.
|
|
140
|
+
|
|
141
|
+
## Reporting an issue
|
|
142
|
+
|
|
143
|
+
If you observe telemetry that you believe contradicts this contract, please open an issue at https://github.com/cacheplane/angular-agent-framework/issues — security-tagged. We treat it as a P0.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
const THREADPLANE_TELEMETRY_CONFIG = new InjectionToken('THREADPLANE_TELEMETRY_CONFIG');
|
|
5
|
+
|
|
6
|
+
function normalizeSampleRate(sampleRate) {
|
|
7
|
+
if (sampleRate === undefined)
|
|
8
|
+
return 1;
|
|
9
|
+
if (!Number.isFinite(sampleRate))
|
|
10
|
+
return 1;
|
|
11
|
+
if (sampleRate <= 0)
|
|
12
|
+
return 0;
|
|
13
|
+
if (sampleRate >= 1)
|
|
14
|
+
return 1;
|
|
15
|
+
return sampleRate;
|
|
16
|
+
}
|
|
17
|
+
function errorClass(error) {
|
|
18
|
+
if (error instanceof Error && error.name)
|
|
19
|
+
return error.name;
|
|
20
|
+
if (error && typeof error === 'object' && 'name' in error && typeof error.name === 'string') {
|
|
21
|
+
return error.name;
|
|
22
|
+
}
|
|
23
|
+
return 'UnknownError';
|
|
24
|
+
}
|
|
25
|
+
class ThreadplaneTelemetryService {
|
|
26
|
+
config = inject(THREADPLANE_TELEMETRY_CONFIG, { optional: true });
|
|
27
|
+
postHogPromise = null;
|
|
28
|
+
distinctId = null;
|
|
29
|
+
async capture(event, properties) {
|
|
30
|
+
if (!this.config?.enabled)
|
|
31
|
+
return;
|
|
32
|
+
const sampleRate = normalizeSampleRate(this.config.sampleRate);
|
|
33
|
+
if (sampleRate === 0)
|
|
34
|
+
return;
|
|
35
|
+
if (sampleRate < 1 && Math.random() >= sampleRate)
|
|
36
|
+
return;
|
|
37
|
+
const enrichedProperties = {
|
|
38
|
+
...(properties ?? {}),
|
|
39
|
+
sample_weight: properties?.['sample_weight'] ?? 1 / sampleRate,
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
if (this.config.sink) {
|
|
43
|
+
await this.config.sink({ event, properties: enrichedProperties });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (this.config.endpoint) {
|
|
47
|
+
await this.captureEndpoint(event, enrichedProperties);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!this.config.posthogKey)
|
|
51
|
+
return;
|
|
52
|
+
const ph = await this.loadPostHog();
|
|
53
|
+
if (!ph)
|
|
54
|
+
return;
|
|
55
|
+
ph.capture(event, enrichedProperties);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// silent fail
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
captureRuntimeInstanceCreated(input) {
|
|
62
|
+
return this.capture('ngaf:runtime_instance_created', { ...input });
|
|
63
|
+
}
|
|
64
|
+
captureRuntimeRequestCreated(input) {
|
|
65
|
+
return this.capture('ngaf:runtime_request_created', { ...input });
|
|
66
|
+
}
|
|
67
|
+
captureStreamStarted(input) {
|
|
68
|
+
return this.capture('ngaf:stream_started', { ...input });
|
|
69
|
+
}
|
|
70
|
+
captureStreamEnded(input) {
|
|
71
|
+
return this.capture('ngaf:stream_ended', { ...input });
|
|
72
|
+
}
|
|
73
|
+
captureStreamErrored(input) {
|
|
74
|
+
const { error, ...rest } = input;
|
|
75
|
+
return this.capture('ngaf:stream_errored', {
|
|
76
|
+
...rest,
|
|
77
|
+
errorClass: errorClass(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async captureEndpoint(event, properties) {
|
|
81
|
+
if (typeof fetch !== 'function' || !this.config?.endpoint)
|
|
82
|
+
return;
|
|
83
|
+
await fetch(this.config.endpoint, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json' },
|
|
86
|
+
keepalive: true,
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
event,
|
|
89
|
+
distinctId: this.getDistinctId(),
|
|
90
|
+
properties,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
getDistinctId() {
|
|
95
|
+
if (!this.distinctId) {
|
|
96
|
+
const cryptoApi = globalThis.crypto;
|
|
97
|
+
const value = typeof cryptoApi?.randomUUID === 'function'
|
|
98
|
+
? cryptoApi.randomUUID()
|
|
99
|
+
: Math.random().toString(36).slice(2, 12);
|
|
100
|
+
this.distinctId = `browser:${value}`;
|
|
101
|
+
}
|
|
102
|
+
return this.distinctId;
|
|
103
|
+
}
|
|
104
|
+
loadPostHog() {
|
|
105
|
+
if (!this.postHogPromise) {
|
|
106
|
+
this.postHogPromise = import('posthog-js').then((mod) => {
|
|
107
|
+
if (!this.config?.posthogKey)
|
|
108
|
+
return null;
|
|
109
|
+
mod.default.init(this.config.posthogKey, {
|
|
110
|
+
api_host: this.config.posthogHost ?? 'https://us.i.posthog.com',
|
|
111
|
+
});
|
|
112
|
+
return mod.default;
|
|
113
|
+
}).catch(() => null);
|
|
114
|
+
}
|
|
115
|
+
return this.postHogPromise;
|
|
116
|
+
}
|
|
117
|
+
static ɵfac = function ThreadplaneTelemetryService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ThreadplaneTelemetryService)(); };
|
|
118
|
+
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ThreadplaneTelemetryService, factory: ThreadplaneTelemetryService.ɵfac, providedIn: 'root' });
|
|
119
|
+
}
|
|
120
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ThreadplaneTelemetryService, [{
|
|
121
|
+
type: Injectable,
|
|
122
|
+
args: [{ providedIn: 'root' }]
|
|
123
|
+
}], null, null); })();
|
|
124
|
+
|
|
125
|
+
function provideThreadplaneTelemetry(config) {
|
|
126
|
+
return makeEnvironmentProviders([
|
|
127
|
+
{ provide: THREADPLANE_TELEMETRY_CONFIG, useValue: config },
|
|
128
|
+
ThreadplaneTelemetryService,
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// SPDX-License-Identifier: MIT
|
|
133
|
+
// Note: kept self-contained (no `../shared` import) so ng-packagr can compile
|
|
134
|
+
// this entry point in isolation. The trim+truncate hardening is duplicated
|
|
135
|
+
// from shared/properties.ts on purpose — it's a few lines, and the alternative
|
|
136
|
+
// is a secondary ng-packagr entry point for shared, which is heavier.
|
|
137
|
+
function toSafeString(value, maxLength) {
|
|
138
|
+
if (typeof value !== 'string')
|
|
139
|
+
return undefined;
|
|
140
|
+
const trimmed = value.trim();
|
|
141
|
+
if (!trimmed)
|
|
142
|
+
return undefined;
|
|
143
|
+
return trimmed.slice(0, maxLength);
|
|
144
|
+
}
|
|
145
|
+
function isLocalAnalyticsHost(host) {
|
|
146
|
+
const value = toSafeString(host, 300)?.toLowerCase();
|
|
147
|
+
if (!value)
|
|
148
|
+
return false;
|
|
149
|
+
// IPv6 literal `::1` or `[::1]:port` — don't naively split on `:`.
|
|
150
|
+
if (value === '::1' || value.startsWith('[::1]'))
|
|
151
|
+
return true;
|
|
152
|
+
const hostname = value.split(':')[0];
|
|
153
|
+
return hostname === 'localhost' || hostname === '127.0.0.1';
|
|
154
|
+
}
|
|
155
|
+
function shouldCaptureAnalytics({ token, captureLocal = false, host }) {
|
|
156
|
+
if (!toSafeString(token, 500))
|
|
157
|
+
return false;
|
|
158
|
+
if (isLocalAnalyticsHost(host) && !captureLocal)
|
|
159
|
+
return false;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generated bundle index. Do not edit.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
export { THREADPLANE_TELEMETRY_CONFIG, ThreadplaneTelemetryService, isLocalAnalyticsHost, provideThreadplaneTelemetry, shouldCaptureAnalytics };
|
|
168
|
+
//# sourceMappingURL=threadplane-telemetry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threadplane-telemetry.mjs","sources":["../../../../../libs/telemetry/src/browser/tokens.ts","../../../../../libs/telemetry/src/browser/service.ts","../../../../../libs/telemetry/src/browser/provide.ts","../../../../../libs/telemetry/src/browser/properties.ts","../../../../../libs/telemetry/src/browser/threadplane-telemetry.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\nexport type ThreadplaneTelemetryEvent =\n | 'ngaf:browser_provided'\n | 'ngaf:browser_chat_init'\n | 'ngaf:runtime_instance_created'\n | 'ngaf:runtime_request_created'\n | 'ngaf:stream_started'\n | 'ngaf:stream_ended'\n | 'ngaf:stream_errored';\n\nexport interface ThreadplaneTelemetryEventPayload {\n event: ThreadplaneTelemetryEvent;\n properties?: Record<string, unknown>;\n}\n\nexport type ThreadplaneTelemetrySink = (payload: ThreadplaneTelemetryEventPayload) => void | Promise<void>;\n\nexport interface ThreadplaneTelemetryConfig {\n enabled: boolean;\n /**\n * Preferred app-owned delivery hook. Use this when the consuming app wants\n * to forward events through its own analytics boundary.\n */\n sink?: ThreadplaneTelemetrySink;\n /**\n * Preferred app-owned ingest URL. The browser service POSTs neutral event\n * payloads here; the endpoint decides where they ultimately go.\n */\n endpoint?: string;\n /** @deprecated Prefer sink or endpoint so public app code is vendor-neutral. */\n posthogKey?: string;\n /** @deprecated Prefer sink or endpoint so public app code is vendor-neutral. */\n posthogHost?: string;\n sampleRate?: number;\n}\n\nexport const THREADPLANE_TELEMETRY_CONFIG = new InjectionToken<ThreadplaneTelemetryConfig | null>(\n 'THREADPLANE_TELEMETRY_CONFIG',\n);\n","import { Injectable, inject } from '@angular/core';\nimport {\n THREADPLANE_TELEMETRY_CONFIG,\n type ThreadplaneTelemetryConfig,\n type ThreadplaneTelemetryEvent,\n} from './tokens';\n\n// Inlined from shared/events.ts: ng-packagr enforces rootDir at the entry-file\n// level (src/browser/), so the browser entry cannot import from ../shared/.\n// Keep this type in sync with shared/events.ts.\nexport type ThreadplaneBrowserEvent = ThreadplaneTelemetryEvent;\n\nexport interface ThreadplaneBrowserRuntimeTelemetry {\n transport: string;\n surface?: string;\n provider?: string;\n model?: string;\n}\n\nexport interface ThreadplaneBrowserStreamTelemetry extends ThreadplaneBrowserRuntimeTelemetry {\n durationMs?: number;\n}\n\nexport interface ThreadplaneBrowserStreamErrorTelemetry extends ThreadplaneBrowserStreamTelemetry {\n error?: unknown;\n}\n\nfunction normalizeSampleRate(sampleRate: number | undefined): number {\n if (sampleRate === undefined) return 1;\n if (!Number.isFinite(sampleRate)) return 1;\n if (sampleRate <= 0) return 0;\n if (sampleRate >= 1) return 1;\n return sampleRate;\n}\n\nfunction errorClass(error: unknown): string {\n if (error instanceof Error && error.name) return error.name;\n if (error && typeof error === 'object' && 'name' in error && typeof error.name === 'string') {\n return error.name;\n }\n return 'UnknownError';\n}\n\n@Injectable({ providedIn: 'root' })\nexport class ThreadplaneTelemetryService {\n private config: ThreadplaneTelemetryConfig | null = inject(THREADPLANE_TELEMETRY_CONFIG, { optional: true });\n private postHogPromise: Promise<typeof import('posthog-js')['default'] | null> | null = null;\n private distinctId: string | null = null;\n\n async capture(event: ThreadplaneTelemetryEvent, properties?: Record<string, unknown>): Promise<void> {\n if (!this.config?.enabled) return;\n const sampleRate = normalizeSampleRate(this.config.sampleRate);\n if (sampleRate === 0) return;\n if (sampleRate < 1 && Math.random() >= sampleRate) return;\n\n const enrichedProperties = {\n ...(properties ?? {}),\n sample_weight: properties?.['sample_weight'] ?? 1 / sampleRate,\n };\n\n try {\n if (this.config.sink) {\n await this.config.sink({ event, properties: enrichedProperties });\n return;\n }\n if (this.config.endpoint) {\n await this.captureEndpoint(event, enrichedProperties);\n return;\n }\n if (!this.config.posthogKey) return;\n const ph = await this.loadPostHog();\n if (!ph) return;\n ph.capture(event, enrichedProperties);\n } catch {\n // silent fail\n }\n }\n\n captureRuntimeInstanceCreated(input: ThreadplaneBrowserRuntimeTelemetry): Promise<void> {\n return this.capture('ngaf:runtime_instance_created', { ...input });\n }\n\n captureRuntimeRequestCreated(input: ThreadplaneBrowserRuntimeTelemetry & { requestType: string }): Promise<void> {\n return this.capture('ngaf:runtime_request_created', { ...input });\n }\n\n captureStreamStarted(input: ThreadplaneBrowserStreamTelemetry): Promise<void> {\n return this.capture('ngaf:stream_started', { ...input });\n }\n\n captureStreamEnded(input: ThreadplaneBrowserStreamTelemetry): Promise<void> {\n return this.capture('ngaf:stream_ended', { ...input });\n }\n\n captureStreamErrored(input: ThreadplaneBrowserStreamErrorTelemetry): Promise<void> {\n const { error, ...rest } = input;\n return this.capture('ngaf:stream_errored', {\n ...rest,\n errorClass: errorClass(error),\n });\n }\n\n private async captureEndpoint(event: ThreadplaneTelemetryEvent, properties: Record<string, unknown>): Promise<void> {\n if (typeof fetch !== 'function' || !this.config?.endpoint) return;\n await fetch(this.config.endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n keepalive: true,\n body: JSON.stringify({\n event,\n distinctId: this.getDistinctId(),\n properties,\n }),\n });\n }\n\n private getDistinctId(): string {\n if (!this.distinctId) {\n const cryptoApi = globalThis.crypto as Crypto | undefined;\n const value = typeof cryptoApi?.randomUUID === 'function'\n ? cryptoApi.randomUUID()\n : Math.random().toString(36).slice(2, 12);\n this.distinctId = `browser:${value}`;\n }\n return this.distinctId;\n }\n\n private loadPostHog(): Promise<typeof import('posthog-js')['default'] | null> {\n if (!this.postHogPromise) {\n this.postHogPromise = import('posthog-js').then((mod) => {\n if (!this.config?.posthogKey) return null;\n mod.default.init(this.config.posthogKey, {\n api_host: this.config.posthogHost ?? 'https://us.i.posthog.com',\n });\n return mod.default;\n }).catch(() => null);\n }\n return this.postHogPromise;\n }\n}\n","import { makeEnvironmentProviders, type EnvironmentProviders } from '@angular/core';\nimport { THREADPLANE_TELEMETRY_CONFIG, type ThreadplaneTelemetryConfig } from './tokens';\nimport { ThreadplaneTelemetryService } from './service';\n\nexport function provideThreadplaneTelemetry(config: ThreadplaneTelemetryConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: THREADPLANE_TELEMETRY_CONFIG, useValue: config },\n ThreadplaneTelemetryService,\n ]);\n}\n","// SPDX-License-Identifier: MIT\n// Note: kept self-contained (no `../shared` import) so ng-packagr can compile\n// this entry point in isolation. The trim+truncate hardening is duplicated\n// from shared/properties.ts on purpose — it's a few lines, and the alternative\n// is a secondary ng-packagr entry point for shared, which is heavier.\nfunction toSafeString(value: unknown, maxLength: number): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n return trimmed.slice(0, maxLength);\n}\n\nexport type CaptureConfig = {\n token?: string;\n captureLocal?: boolean;\n host?: string;\n};\n\nexport function isLocalAnalyticsHost(host: unknown): boolean {\n const value = toSafeString(host, 300)?.toLowerCase();\n if (!value) return false;\n\n // IPv6 literal `::1` or `[::1]:port` — don't naively split on `:`.\n if (value === '::1' || value.startsWith('[::1]')) return true;\n\n const hostname = value.split(':')[0];\n return hostname === 'localhost' || hostname === '127.0.0.1';\n}\n\nexport function shouldCaptureAnalytics({ token, captureLocal = false, host }: CaptureConfig): boolean {\n if (!toSafeString(token, 500)) return false;\n if (isLocalAnalyticsHost(host) && !captureLocal) return false;\n return true;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './types/threadplane-telemetry';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, EnvironmentProviders } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
type ThreadplaneTelemetryEvent = 'ngaf:browser_provided' | 'ngaf:browser_chat_init' | 'ngaf:runtime_instance_created' | 'ngaf:runtime_request_created' | 'ngaf:stream_started' | 'ngaf:stream_ended' | 'ngaf:stream_errored';
|
|
5
|
+
interface ThreadplaneTelemetryEventPayload {
|
|
6
|
+
event: ThreadplaneTelemetryEvent;
|
|
7
|
+
properties?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
type ThreadplaneTelemetrySink = (payload: ThreadplaneTelemetryEventPayload) => void | Promise<void>;
|
|
10
|
+
interface ThreadplaneTelemetryConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Preferred app-owned delivery hook. Use this when the consuming app wants
|
|
14
|
+
* to forward events through its own analytics boundary.
|
|
15
|
+
*/
|
|
16
|
+
sink?: ThreadplaneTelemetrySink;
|
|
17
|
+
/**
|
|
18
|
+
* Preferred app-owned ingest URL. The browser service POSTs neutral event
|
|
19
|
+
* payloads here; the endpoint decides where they ultimately go.
|
|
20
|
+
*/
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
/** @deprecated Prefer sink or endpoint so public app code is vendor-neutral. */
|
|
23
|
+
posthogKey?: string;
|
|
24
|
+
/** @deprecated Prefer sink or endpoint so public app code is vendor-neutral. */
|
|
25
|
+
posthogHost?: string;
|
|
26
|
+
sampleRate?: number;
|
|
27
|
+
}
|
|
28
|
+
declare const THREADPLANE_TELEMETRY_CONFIG: InjectionToken<ThreadplaneTelemetryConfig | null>;
|
|
29
|
+
|
|
30
|
+
declare function provideThreadplaneTelemetry(config: ThreadplaneTelemetryConfig): EnvironmentProviders;
|
|
31
|
+
|
|
32
|
+
type ThreadplaneBrowserEvent = ThreadplaneTelemetryEvent;
|
|
33
|
+
interface ThreadplaneBrowserRuntimeTelemetry {
|
|
34
|
+
transport: string;
|
|
35
|
+
surface?: string;
|
|
36
|
+
provider?: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
}
|
|
39
|
+
interface ThreadplaneBrowserStreamTelemetry extends ThreadplaneBrowserRuntimeTelemetry {
|
|
40
|
+
durationMs?: number;
|
|
41
|
+
}
|
|
42
|
+
interface ThreadplaneBrowserStreamErrorTelemetry extends ThreadplaneBrowserStreamTelemetry {
|
|
43
|
+
error?: unknown;
|
|
44
|
+
}
|
|
45
|
+
declare class ThreadplaneTelemetryService {
|
|
46
|
+
private config;
|
|
47
|
+
private postHogPromise;
|
|
48
|
+
private distinctId;
|
|
49
|
+
capture(event: ThreadplaneTelemetryEvent, properties?: Record<string, unknown>): Promise<void>;
|
|
50
|
+
captureRuntimeInstanceCreated(input: ThreadplaneBrowserRuntimeTelemetry): Promise<void>;
|
|
51
|
+
captureRuntimeRequestCreated(input: ThreadplaneBrowserRuntimeTelemetry & {
|
|
52
|
+
requestType: string;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
captureStreamStarted(input: ThreadplaneBrowserStreamTelemetry): Promise<void>;
|
|
55
|
+
captureStreamEnded(input: ThreadplaneBrowserStreamTelemetry): Promise<void>;
|
|
56
|
+
captureStreamErrored(input: ThreadplaneBrowserStreamErrorTelemetry): Promise<void>;
|
|
57
|
+
private captureEndpoint;
|
|
58
|
+
private getDistinctId;
|
|
59
|
+
private loadPostHog;
|
|
60
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ThreadplaneTelemetryService, never>;
|
|
61
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ThreadplaneTelemetryService>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type CaptureConfig = {
|
|
65
|
+
token?: string;
|
|
66
|
+
captureLocal?: boolean;
|
|
67
|
+
host?: string;
|
|
68
|
+
};
|
|
69
|
+
declare function isLocalAnalyticsHost(host: unknown): boolean;
|
|
70
|
+
declare function shouldCaptureAnalytics({ token, captureLocal, host }: CaptureConfig): boolean;
|
|
71
|
+
|
|
72
|
+
export { THREADPLANE_TELEMETRY_CONFIG, ThreadplaneTelemetryService, isLocalAnalyticsHost, provideThreadplaneTelemetry, shouldCaptureAnalytics };
|
|
73
|
+
export type { CaptureConfig, ThreadplaneBrowserEvent, ThreadplaneBrowserRuntimeTelemetry, ThreadplaneBrowserStreamErrorTelemetry, ThreadplaneBrowserStreamTelemetry, ThreadplaneTelemetryConfig, ThreadplaneTelemetryEvent, ThreadplaneTelemetryEventPayload, ThreadplaneTelemetrySink };
|
|
74
|
+
//# sourceMappingURL=threadplane-telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threadplane-telemetry.d.ts","sources":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","names":[]}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { isTelemetryDisabled, getDisableReason } from './shared/env.js';
|
|
2
|
+
export { sha256 } from './shared/hash.js';
|
|
3
|
+
export { getAnonId } from './shared/anon-id.js';
|
|
4
|
+
export { shouldSample } from './shared/sample.js';
|
|
5
|
+
export type { ThreadplaneEvent, ThreadplaneNodeEvent, ThreadplaneBrowserEvent } from './shared/events.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/telemetry/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface RuntimeInstanceTelemetry {
|
|
2
|
+
transport: string;
|
|
3
|
+
provider?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
angularVersion?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StreamTelemetry {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RuntimeRequestTelemetry {
|
|
14
|
+
transport: string;
|
|
15
|
+
requestType: string;
|
|
16
|
+
provider?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function captureRuntimeInstanceCreated(input: RuntimeInstanceTelemetry): Promise<void>;
|
|
20
|
+
export declare function captureRuntimeRequestCreated(input: RuntimeRequestTelemetry): Promise<void>;
|
|
21
|
+
export declare function captureStreamStarted(input: StreamTelemetry): Promise<void>;
|
|
22
|
+
export declare function captureStreamEnded(input: StreamTelemetry): Promise<void>;
|
|
23
|
+
export declare function captureStreamErrored(input: StreamTelemetry & {
|
|
24
|
+
error: Error | unknown;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/node/adapter.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAsB,6BAA6B,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMlG;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhG;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhF;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
package/node/adapter.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { captureEvent } from './client.js';
|
|
2
|
+
async function safe(fn) {
|
|
3
|
+
try {
|
|
4
|
+
await fn();
|
|
5
|
+
}
|
|
6
|
+
catch { /* silent fail */ }
|
|
7
|
+
}
|
|
8
|
+
export async function captureRuntimeInstanceCreated(input) {
|
|
9
|
+
await safe(async () => {
|
|
10
|
+
const { apiKey, ...rest } = input;
|
|
11
|
+
void apiKey;
|
|
12
|
+
await captureEvent('ngaf:runtime_instance_created', { ...rest });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function captureRuntimeRequestCreated(input) {
|
|
16
|
+
await safe(() => captureEvent('ngaf:runtime_request_created', { ...input }));
|
|
17
|
+
}
|
|
18
|
+
export async function captureStreamStarted(input) {
|
|
19
|
+
await safe(() => captureEvent('ngaf:stream_started', { ...input }));
|
|
20
|
+
}
|
|
21
|
+
export async function captureStreamEnded(input) {
|
|
22
|
+
await safe(() => captureEvent('ngaf:stream_ended', { ...input }));
|
|
23
|
+
}
|
|
24
|
+
export async function captureStreamErrored(input) {
|
|
25
|
+
await safe(async () => {
|
|
26
|
+
const { error, ...rest } = input;
|
|
27
|
+
const errorClass = error instanceof Error ? error.constructor.name : 'Unknown';
|
|
28
|
+
await captureEvent('ngaf:stream_errored', { ...rest, errorClass });
|
|
29
|
+
});
|
|
30
|
+
}
|
package/node/client.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ThreadplaneNodeEvent } from '../shared/events.js';
|
|
2
|
+
export type CaptureResult = {
|
|
3
|
+
sent: true;
|
|
4
|
+
} | {
|
|
5
|
+
sent: false;
|
|
6
|
+
reason: 'disabled' | 'sampled' | 'failed';
|
|
7
|
+
};
|
|
8
|
+
export interface PostinstallInput {
|
|
9
|
+
pkg: string;
|
|
10
|
+
version: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createPostinstallProperties(input: PostinstallInput, env?: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
13
|
+
export declare function captureEvent(event: ThreadplaneNodeEvent, properties?: Record<string, unknown>): Promise<CaptureResult>;
|
|
14
|
+
export declare function capturePostinstall(input: PostinstallInput): Promise<CaptureResult>;
|
|
15
|
+
export declare function _resetClientForTesting(): void;
|
|
16
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/node/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAShE,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;CAAE,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAqDD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gBAAgB,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB;AAkBD,wBAAsB,YAAY,CAChC,KAAK,EAAE,oBAAoB,EAC3B,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACvC,OAAO,CAAC,aAAa,CAAC,CAoBxB;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,aAAa,CAAC,CAExB;AAGD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
package/node/client.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getAnonId } from '../shared/anon-id.js';
|
|
2
|
+
import { isTelemetryDisabled } from '../shared/env.js';
|
|
3
|
+
import { shouldSample } from '../shared/sample.js';
|
|
4
|
+
import { isProgrammaticallyDisabled } from './disable.js';
|
|
5
|
+
const DEFAULT_INGEST = 'https://threadplane.ai/api/ingest';
|
|
6
|
+
const REQUEST_TIMEOUT_MS = 3_000;
|
|
7
|
+
// Public identifier accepted by the Threadplane ingest proxy. The proxy re-keys
|
|
8
|
+
// server-side with the private PostHog token.
|
|
9
|
+
const PUBLIC_INGEST_KEY = 'phc_public_cacheplane_telemetry';
|
|
10
|
+
function getSampleRate(env = process.env) {
|
|
11
|
+
const parsed = Number(env.NGAF_TELEMETRY_SAMPLE_RATE ?? '1');
|
|
12
|
+
if (!Number.isFinite(parsed))
|
|
13
|
+
return 1;
|
|
14
|
+
return Math.max(0, Math.min(1, parsed));
|
|
15
|
+
}
|
|
16
|
+
function readBooleanToken(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (/^(1|true|yes)$/i.test(value))
|
|
20
|
+
return true;
|
|
21
|
+
if (/^(0|false|no)$/i.test(value))
|
|
22
|
+
return false;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function getPackageManager(env = process.env) {
|
|
26
|
+
const userAgent = env.npm_config_user_agent;
|
|
27
|
+
const tokens = userAgent?.split(/\s+/).filter(Boolean) ?? [];
|
|
28
|
+
const firstToken = tokens[0];
|
|
29
|
+
const match = firstToken?.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
30
|
+
if (!match)
|
|
31
|
+
return {};
|
|
32
|
+
const out = {
|
|
33
|
+
package_manager: match[1],
|
|
34
|
+
package_manager_version: match[2],
|
|
35
|
+
};
|
|
36
|
+
const nodeTokenIndex = tokens.findIndex((token) => token.startsWith('node/'));
|
|
37
|
+
const nodeToken = nodeTokenIndex >= 0 ? tokens[nodeTokenIndex] : undefined;
|
|
38
|
+
const nodeVersion = nodeToken?.match(/^node\/([^/\s]+)$/)?.[1];
|
|
39
|
+
if (nodeVersion)
|
|
40
|
+
out.package_manager_node_version = nodeVersion.replace(/^v/, '');
|
|
41
|
+
if (nodeTokenIndex >= 0) {
|
|
42
|
+
const platformTokens = tokens
|
|
43
|
+
.slice(nodeTokenIndex + 1)
|
|
44
|
+
.filter((token) => !token.includes('/'));
|
|
45
|
+
if (platformTokens[0])
|
|
46
|
+
out.package_manager_os = platformTokens[0];
|
|
47
|
+
if (platformTokens[1])
|
|
48
|
+
out.package_manager_arch = platformTokens[1];
|
|
49
|
+
}
|
|
50
|
+
const workspacesValue = tokens
|
|
51
|
+
.find((token) => token.startsWith('workspaces/'))
|
|
52
|
+
?.split('/')[1];
|
|
53
|
+
const workspaces = readBooleanToken(workspacesValue);
|
|
54
|
+
if (workspaces !== undefined)
|
|
55
|
+
out.package_manager_workspaces = workspaces;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
// @internal
|
|
59
|
+
export function createPostinstallProperties(input, env = process.env) {
|
|
60
|
+
return {
|
|
61
|
+
pkg: input.pkg,
|
|
62
|
+
version: input.version,
|
|
63
|
+
node: process.version,
|
|
64
|
+
node_version: process.version,
|
|
65
|
+
os: process.platform,
|
|
66
|
+
arch: process.arch,
|
|
67
|
+
global_install: readBooleanToken(env.npm_config_global) === true ||
|
|
68
|
+
env.npm_config_location === 'global',
|
|
69
|
+
...getPackageManager(env),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function postJson(url, body) {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new Error(`telemetry ingest failed: ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function captureEvent(event, properties = {}) {
|
|
90
|
+
if (isTelemetryDisabled() || isProgrammaticallyDisabled())
|
|
91
|
+
return { sent: false, reason: 'disabled' };
|
|
92
|
+
const rate = getSampleRate();
|
|
93
|
+
const anonId = getAnonId();
|
|
94
|
+
if (!shouldSample(rate, anonId))
|
|
95
|
+
return { sent: false, reason: 'sampled' };
|
|
96
|
+
try {
|
|
97
|
+
await postJson(process.env.NGAF_TELEMETRY_INGEST_URL ?? DEFAULT_INGEST, {
|
|
98
|
+
key: PUBLIC_INGEST_KEY,
|
|
99
|
+
distinctId: anonId,
|
|
100
|
+
event,
|
|
101
|
+
properties: {
|
|
102
|
+
...properties,
|
|
103
|
+
sample_weight: rate > 0 ? 1 / Math.min(1, rate) : 1,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return { sent: true };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { sent: false, reason: 'failed' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function capturePostinstall(input) {
|
|
113
|
+
return captureEvent('ngaf:postinstall', createPostinstallProperties(input));
|
|
114
|
+
}
|
|
115
|
+
// @internal — tests only
|
|
116
|
+
export function _resetClientForTesting() {
|
|
117
|
+
// retained for older tests and downstream test helpers
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disable.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/node/disable.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAGD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|
package/node/disable.js
ADDED
package/node/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { disableTelemetry } from './disable.js';
|
|
2
|
+
export { capturePostinstall, captureEvent } from './client.js';
|
|
3
|
+
export type { CaptureResult } from './client.js';
|
|
4
|
+
export { captureRuntimeInstanceCreated, captureRuntimeRequestCreated, captureStreamStarted, captureStreamEnded, captureStreamErrored, } from './adapter.js';
|
|
5
|
+
export type { RuntimeInstanceTelemetry, RuntimeRequestTelemetry, StreamTelemetry } from './adapter.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAC5B,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|
package/node/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface PostinstallDeps {
|
|
3
|
+
readPackageJson: () => {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
write: (s: string) => void;
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
cwd?: () => string;
|
|
10
|
+
}
|
|
11
|
+
export declare function capturePostinstallScript(deps: PostinstallDeps): Promise<void>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=postinstall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/node/postinstall.ts"],"names":[],"mappings":";AAOA,UAAU,eAAe;IACvB,eAAe,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AA4CD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCnF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { join, resolve, sep } from 'node:path';
|
|
5
|
+
import { capturePostinstall, createPostinstallProperties } from './client.js';
|
|
6
|
+
import { isTelemetryDisabled } from '../shared/env.js';
|
|
7
|
+
const TRUE_VALUES = new Set(['1', 'true', 'TRUE', 'yes']);
|
|
8
|
+
function truthy(value) {
|
|
9
|
+
return value !== undefined && TRUE_VALUES.has(value);
|
|
10
|
+
}
|
|
11
|
+
function debugEnabled(env) {
|
|
12
|
+
return (env.DEBUG ?? '')
|
|
13
|
+
.split(/[\s,]+/)
|
|
14
|
+
.some((part) => part === '*' || part === 'ngaf:*' || part === 'ngaf:telemetry');
|
|
15
|
+
}
|
|
16
|
+
function isGlobalInstall(env) {
|
|
17
|
+
return truthy(env.npm_config_global) || env.npm_config_location === 'global';
|
|
18
|
+
}
|
|
19
|
+
function canonicalPath(path) {
|
|
20
|
+
try {
|
|
21
|
+
return realpathSync(path);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return resolve(path);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isInNodeModules(cwd) {
|
|
28
|
+
return canonicalPath(cwd).split(sep).includes('node_modules');
|
|
29
|
+
}
|
|
30
|
+
function shouldSkipLocalTopLevelInstall(env, cwd) {
|
|
31
|
+
if (isGlobalInstall(env))
|
|
32
|
+
return false;
|
|
33
|
+
const resolvedCwd = canonicalPath(cwd);
|
|
34
|
+
if (env.INIT_CWD && canonicalPath(env.INIT_CWD) === resolvedCwd)
|
|
35
|
+
return true;
|
|
36
|
+
return !isInNodeModules(resolvedCwd);
|
|
37
|
+
}
|
|
38
|
+
function readLifecyclePackageJson(env, cwd) {
|
|
39
|
+
if (env.npm_package_name && env.npm_package_version) {
|
|
40
|
+
return { name: env.npm_package_name, version: env.npm_package_version };
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
43
|
+
}
|
|
44
|
+
export async function capturePostinstallScript(deps) {
|
|
45
|
+
// Single opt-out gate. DO_NOT_TRACK, npm_config_do_not_track,
|
|
46
|
+
// NGAF_TELEMETRY_DISABLED, and CI envs all return early: no event, no notice.
|
|
47
|
+
if (isTelemetryDisabled(deps.env))
|
|
48
|
+
return;
|
|
49
|
+
const cwd = deps.cwd?.() ?? process.cwd();
|
|
50
|
+
if (shouldSkipLocalTopLevelInstall(deps.env, cwd))
|
|
51
|
+
return;
|
|
52
|
+
let pkg;
|
|
53
|
+
try {
|
|
54
|
+
pkg = deps.readPackageJson();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const result = await capturePostinstall({ pkg: pkg.name, version: pkg.version });
|
|
61
|
+
if (debugEnabled(deps.env)) {
|
|
62
|
+
deps.write(`@threadplane/telemetry payload: ${JSON.stringify({
|
|
63
|
+
event: 'ngaf:postinstall',
|
|
64
|
+
properties: createPostinstallProperties({ pkg: pkg.name, version: pkg.version }, deps.env),
|
|
65
|
+
})}\n`);
|
|
66
|
+
}
|
|
67
|
+
if (result.sent) {
|
|
68
|
+
deps.write(`@threadplane/telemetry: install telemetry sent (${pkg.name}@${pkg.version}). ` +
|
|
69
|
+
`Disable: DO_NOT_TRACK=1 or NGAF_TELEMETRY_DISABLED=1. ` +
|
|
70
|
+
`See https://github.com/cacheplane/angular-agent-framework/blob/main/libs/telemetry/README.md\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// never break npm install
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function flushStdout() {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
if (process.stdout.writableNeedDrain) {
|
|
80
|
+
process.stdout.once('drain', () => resolve());
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
setImmediate(() => resolve());
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Entry point — invoked by package.json scripts.postinstall.
|
|
88
|
+
async function main() {
|
|
89
|
+
await capturePostinstallScript({
|
|
90
|
+
readPackageJson: () => readLifecyclePackageJson(process.env, process.cwd()),
|
|
91
|
+
write: (s) => process.stdout.write(s),
|
|
92
|
+
env: process.env,
|
|
93
|
+
cwd: () => process.cwd(),
|
|
94
|
+
});
|
|
95
|
+
await flushStdout();
|
|
96
|
+
}
|
|
97
|
+
// Only run as main entry, not when imported by tests.
|
|
98
|
+
function isDirectRun() {
|
|
99
|
+
const entry = process.argv[1];
|
|
100
|
+
if (!entry)
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
return pathToFileURL(realpathSync(entry)).href === import.meta.url;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (isDirectRun())
|
|
110
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@threadplane/telemetry",
|
|
3
|
+
"version": "0.0.46",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/cacheplane/angular-agent-framework.git",
|
|
11
|
+
"directory": "libs/telemetry"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/cacheplane/angular-agent-framework#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/cacheplane/angular-agent-framework/issues"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"threadplane-telemetry-postinstall": "./node/postinstall.js"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./index.d.ts",
|
|
25
|
+
"default": "./index.js"
|
|
26
|
+
},
|
|
27
|
+
"./shared": {
|
|
28
|
+
"types": "./shared/public-api.d.ts",
|
|
29
|
+
"default": "./shared/public-api.js"
|
|
30
|
+
},
|
|
31
|
+
"./node": {
|
|
32
|
+
"types": "./node/index.d.ts",
|
|
33
|
+
"default": "./node/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./node/postinstall": {
|
|
36
|
+
"types": "./node/postinstall.d.ts",
|
|
37
|
+
"default": "./node/postinstall.js"
|
|
38
|
+
},
|
|
39
|
+
"./browser": {
|
|
40
|
+
"types": "./browser/index.d.ts",
|
|
41
|
+
"default": "./browser/fesm2022/threadplane-telemetry.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./README.md": "./README.md"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@angular/core": "^20.0.0 || ^21.0.0",
|
|
47
|
+
"posthog-js": "^1.372.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@angular/core": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"posthog-js": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"postinstall": "node ./node/postinstall.js || true"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anon-id.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/anon-id.ts"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,IAAI,MAAM,CAGlC;AAGD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
let cached = null;
|
|
3
|
+
export function getAnonId() {
|
|
4
|
+
if (!cached)
|
|
5
|
+
cached = `anon_${randomUUID()}`;
|
|
6
|
+
return cached;
|
|
7
|
+
}
|
|
8
|
+
// @internal — for tests only
|
|
9
|
+
export function _resetAnonIdForTesting() {
|
|
10
|
+
cached = null;
|
|
11
|
+
}
|
package/shared/env.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type DisableReason = 'DO_NOT_TRACK' | 'NGAF_TELEMETRY_DISABLED' | 'CI' | null;
|
|
2
|
+
export declare function getDisableReason(env?: NodeJS.ProcessEnv): DisableReason;
|
|
3
|
+
export declare function isTelemetryDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/env.ts"],"names":[],"mappings":"AAMA,KAAK,aAAa,GAAG,cAAc,GAAG,yBAAyB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9E,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,aAAa,CAepF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEjF"}
|
package/shared/env.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const TRUE_VALUES = new Set(['1', 'true', 'TRUE', 'yes']);
|
|
2
|
+
function truthy(v) {
|
|
3
|
+
return v !== undefined && TRUE_VALUES.has(v);
|
|
4
|
+
}
|
|
5
|
+
export function getDisableReason(env = process.env) {
|
|
6
|
+
if (truthy(env.DO_NOT_TRACK) || truthy(env.npm_config_do_not_track) || truthy(env.NPM_CONFIG_DO_NOT_TRACK)) {
|
|
7
|
+
return 'DO_NOT_TRACK';
|
|
8
|
+
}
|
|
9
|
+
if (truthy(env.NGAF_TELEMETRY_DISABLED))
|
|
10
|
+
return 'NGAF_TELEMETRY_DISABLED';
|
|
11
|
+
if (truthy(env.CI) ||
|
|
12
|
+
truthy(env.GITHUB_ACTIONS) ||
|
|
13
|
+
truthy(env.CONTINUOUS_INTEGRATION) ||
|
|
14
|
+
truthy(env.BUILDKITE) ||
|
|
15
|
+
truthy(env.CIRCLECI)) {
|
|
16
|
+
return 'CI';
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function isTelemetryDisabled(env = process.env) {
|
|
21
|
+
return getDisableReason(env) !== null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ThreadplaneNodeEvent = 'ngaf:postinstall' | 'ngaf:runtime_instance_created' | 'ngaf:runtime_request_created' | 'ngaf:stream_started' | 'ngaf:stream_ended' | 'ngaf:stream_errored';
|
|
2
|
+
export type ThreadplaneBrowserEvent = 'ngaf:browser_provided' | 'ngaf:browser_chat_init';
|
|
3
|
+
export type ThreadplaneEvent = ThreadplaneNodeEvent | ThreadplaneBrowserEvent;
|
|
4
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/events.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,kBAAkB,GAClB,+BAA+B,GAC/B,8BAA8B,GAC9B,qBAAqB,GACrB,mBAAmB,GACnB,qBAAqB,CAAC;AAE1B,MAAM,MAAM,uBAAuB,GAC/B,uBAAuB,GACvB,wBAAwB,CAAC;AAE7B,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,uBAAuB,CAAC"}
|
package/shared/events.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/shared/hash.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/hash.ts"],"names":[],"mappings":"AAAA,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM3D"}
|
package/shared/hash.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personal-email-domains.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/personal-email-domains.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAoBrD,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGhF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export const PERSONAL_EMAIL_DOMAINS = new Set([
|
|
3
|
+
'gmail.com',
|
|
4
|
+
'yahoo.com',
|
|
5
|
+
'hotmail.com',
|
|
6
|
+
'outlook.com',
|
|
7
|
+
'live.com',
|
|
8
|
+
'icloud.com',
|
|
9
|
+
'me.com',
|
|
10
|
+
'mac.com',
|
|
11
|
+
'proton.me',
|
|
12
|
+
'protonmail.com',
|
|
13
|
+
'aol.com',
|
|
14
|
+
'gmx.com',
|
|
15
|
+
'mail.com',
|
|
16
|
+
'yandex.com',
|
|
17
|
+
'fastmail.com',
|
|
18
|
+
'msn.com',
|
|
19
|
+
'qq.com',
|
|
20
|
+
'163.com',
|
|
21
|
+
'126.com',
|
|
22
|
+
]);
|
|
23
|
+
export function isPersonalEmailDomain(domain) {
|
|
24
|
+
if (!domain)
|
|
25
|
+
return false;
|
|
26
|
+
return PERSONAL_EMAIL_DOMAINS.has(domain.toLowerCase());
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function toSafeAnalyticsString(value: unknown, maxLength?: number): string | undefined;
|
|
2
|
+
export declare function getEmailDomain(email: unknown): string | null;
|
|
3
|
+
export declare function getSourcePage(value: unknown): string;
|
|
4
|
+
export declare function normalizePostHogHost(host: unknown): string;
|
|
5
|
+
//# sourceMappingURL=properties.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"properties.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/properties.ts"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,SAAM,GAAG,MAAM,GAAG,SAAS,CAKzF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAS5D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYpD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAI1D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
3
|
+
export function toSafeAnalyticsString(value, maxLength = 200) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return undefined;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
return undefined;
|
|
9
|
+
return trimmed.slice(0, maxLength);
|
|
10
|
+
}
|
|
11
|
+
export function getEmailDomain(email) {
|
|
12
|
+
const value = toSafeAnalyticsString(email, 320);
|
|
13
|
+
if (!value)
|
|
14
|
+
return null;
|
|
15
|
+
const atIndex = value.lastIndexOf('@');
|
|
16
|
+
if (atIndex <= 0 || atIndex === value.length - 1)
|
|
17
|
+
return null;
|
|
18
|
+
const domain = value.slice(atIndex + 1).toLowerCase();
|
|
19
|
+
return domain.includes('.') ? domain : null;
|
|
20
|
+
}
|
|
21
|
+
export function getSourcePage(value) {
|
|
22
|
+
const source = toSafeAnalyticsString(value, 2000);
|
|
23
|
+
if (!source)
|
|
24
|
+
return '/';
|
|
25
|
+
if (source.startsWith('/'))
|
|
26
|
+
return source;
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(source);
|
|
29
|
+
return `${url.pathname}${url.search}${url.hash}` || '/';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '/';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function normalizePostHogHost(host) {
|
|
36
|
+
const value = toSafeAnalyticsString(host, 500);
|
|
37
|
+
if (!value)
|
|
38
|
+
return DEFAULT_POSTHOG_HOST;
|
|
39
|
+
return value.endsWith('/') ? value.slice(0, -1) : value;
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { ThreadplaneEvent, ThreadplaneNodeEvent, ThreadplaneBrowserEvent } from './events';
|
|
2
|
+
export { getEmailDomain, getSourcePage, normalizePostHogHost, toSafeAnalyticsString, } from './properties';
|
|
3
|
+
export { PERSONAL_EMAIL_DOMAINS, isPersonalEmailDomain } from './personal-email-domains';
|
|
4
|
+
//# sourceMappingURL=public-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/public-api.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EACL,cAAc,EACd,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../../../../libs/telemetry/src/shared/sample.ts"],"names":[],"mappings":"AAUA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIlE"}
|
package/shared/sample.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Cheap deterministic 32-bit hash (Fnv-1a) — no crypto needed for sampling.
|
|
2
|
+
function hashString(s) {
|
|
3
|
+
let h = 2166136261;
|
|
4
|
+
for (let i = 0; i < s.length; i++) {
|
|
5
|
+
h ^= s.charCodeAt(i);
|
|
6
|
+
h = (h * 16777619) >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return h;
|
|
9
|
+
}
|
|
10
|
+
export function shouldSample(rate, anonId) {
|
|
11
|
+
if (rate <= 0)
|
|
12
|
+
return false;
|
|
13
|
+
if (rate >= 1)
|
|
14
|
+
return true;
|
|
15
|
+
return hashString(anonId) / 0xffffffff < rate;
|
|
16
|
+
}
|