@latitude-data/openclaw-telemetry 0.0.5 → 0.0.7
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 +106 -23
- package/dist/plugin.js +63 -45
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -12
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -603
- package/dist/cli.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,45 +4,128 @@ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so)
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
Requires OpenClaw **2026.4.25 or newer
|
|
7
|
+
Requires OpenClaw **2026.4.25 or newer**. Get a Latitude API key + project slug from `https://console.latitude.so/settings/api-keys` first.
|
|
8
|
+
|
|
9
|
+
> **One-shot installer coming soon.** A separate `@latitude-data/openclaw-telemetry-cli` package is in flight that will collapse the steps below into `npx -y @latitude-data/openclaw-telemetry-cli install`. For now, the manual flow below is the supported path.
|
|
10
|
+
|
|
11
|
+
### Step 1 — Install the plugin runtime
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @latitude-data/openclaw-telemetry@0.0.7
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pin to an exact version. OpenClaw 2026.4.26's `openclaw security audit --deep` warns about unpinned install specs (`Pin install specs to exact versions … for higher supply-chain stability`), so always include the `@<version>` suffix when installing or upgrading.
|
|
18
|
+
|
|
19
|
+
OpenClaw fetches the package from npm, runs its security scan, copies files into `~/.openclaw/extensions/<id>/`, and creates a (disabled) `plugins.entries["@latitude-data/openclaw-telemetry"]` entry in `~/.openclaw/openclaw.json`. The gateway will print:
|
|
20
|
+
|
|
21
|
+
> Plugin installed but disabled. Configure it, then run `openclaw plugins enable @latitude-data/openclaw-telemetry`.
|
|
22
|
+
|
|
23
|
+
That's expected — the next step does the configure-and-enable.
|
|
24
|
+
|
|
25
|
+
### Step 2 — Configure and enable
|
|
26
|
+
|
|
27
|
+
Run these `openclaw config set` commands (use bracket notation so the scoped package name parses correctly). Substitute your real API key and project slug in the first two:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.apiKey' "lat_xxx"
|
|
31
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.project' "my-project-slug"
|
|
32
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' true
|
|
33
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
|
|
34
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].enabled' true
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Why two `allowConversationAccess` writes? They mean different things — `hooks.*` is OpenClaw's dispatch gate (without it the plugin's hook handlers never fire), `config.*` is the plugin's payload-content gate (without it the plugin emits structural-only spans). For full content capture they must both be `true`. See [The two flags](#the-two-flags) for details.
|
|
38
|
+
|
|
39
|
+
### Step 3 — Add to `plugins.allow` (optional but loud)
|
|
40
|
+
|
|
41
|
+
OpenClaw warns at every gateway restart about non-bundled plugins that auto-load without provenance via `plugins.allow`. Silence the warning by adding the id:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# `config set` can't append to arrays — set the whole list. If you already have
|
|
45
|
+
# entries in `plugins.allow`, include them here too:
|
|
46
|
+
openclaw config set 'plugins.allow' '["@latitude-data/openclaw-telemetry"]'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Step 4 — Restart the gateway
|
|
8
50
|
|
|
9
51
|
```bash
|
|
10
|
-
npx -y @latitude-data/openclaw-telemetry install
|
|
11
52
|
openclaw gateway restart
|
|
12
53
|
```
|
|
13
54
|
|
|
14
|
-
|
|
55
|
+
Verify everything's wired:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openclaw config validate --json
|
|
59
|
+
# → {"valid": true, ...}
|
|
60
|
+
|
|
61
|
+
grep -E "blocked|plugin not found|latitude" /tmp/openclaw/openclaw-*.log | tail
|
|
62
|
+
# → ready (N plugins: ..., @latitude-data/openclaw-telemetry, ...)
|
|
63
|
+
# → no "blocked", no "plugin not found"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Send a message to one of your OpenClaw agents — within a few seconds, traces appear at `https://console.latitude.so/projects/<your-slug>`.
|
|
67
|
+
|
|
68
|
+
### Alternative: hand-edit `~/.openclaw/openclaw.json`
|
|
69
|
+
|
|
70
|
+
If you'd rather paste a JSON block than run six commands, the equivalent edit is:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
{
|
|
74
|
+
"plugins": {
|
|
75
|
+
"allow": ["@latitude-data/openclaw-telemetry"],
|
|
76
|
+
"entries": {
|
|
77
|
+
"@latitude-data/openclaw-telemetry": {
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"hooks": {
|
|
80
|
+
"allowConversationAccess": true
|
|
81
|
+
},
|
|
82
|
+
"config": {
|
|
83
|
+
"apiKey": "lat_xxx",
|
|
84
|
+
"project": "my-project-slug",
|
|
85
|
+
"allowConversationAccess": true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Merge this with whatever else is in `openclaw.json`. Then `openclaw config validate` and `openclaw gateway restart` as above.
|
|
15
94
|
|
|
16
|
-
|
|
17
|
-
2. Hands plugin placement to OpenClaw via `openclaw plugins install <package-path> --force`. OpenClaw copies files into `~/.openclaw/extensions/<id>/`, writes the install record to `~/.openclaw/plugins/installs.json`, and creates the `plugins.entries[id]` block.
|
|
18
|
-
3. Layers our config on top in `~/.openclaw/openclaw.json`:
|
|
19
|
-
- **`config.*`** — credentials, baseUrl, and `allowConversationAccess` (the payload-content gate the plugin's runtime reads).
|
|
20
|
-
- **`hooks.allowConversationAccess`** — the dispatch gate OpenClaw's runtime checks before forwarding LLM/tool/agent events to non-bundled plugins. Always mirrored to the same value as `config.allowConversationAccess`.
|
|
21
|
-
4. Adds the plugin id to `plugins.allow` (running `npx install` is the trust signal). Pass `--no-trust` to opt out.
|
|
95
|
+
### Targeting a different environment
|
|
22
96
|
|
|
23
|
-
|
|
97
|
+
For staging or local-dev, also set `baseUrl`:
|
|
24
98
|
|
|
25
|
-
|
|
99
|
+
```bash
|
|
100
|
+
# Staging:
|
|
101
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "https://staging-ingest.latitude.so"
|
|
102
|
+
|
|
103
|
+
# Local dev:
|
|
104
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "http://localhost:3002"
|
|
105
|
+
```
|
|
26
106
|
|
|
27
|
-
|
|
28
|
-
| --- | --- |
|
|
29
|
-
| `--api-key=<key>` | Pass the API key instead of being prompted. |
|
|
30
|
-
| `--project=<slug>` | Pass the project slug instead of being prompted. |
|
|
31
|
-
| `--staging` | Target `https://staging.latitude.so` / `https://staging-ingest.latitude.so`. |
|
|
32
|
-
| `--dev` | Target `http://localhost:3000` / `http://localhost:3002`. |
|
|
33
|
-
| `--yes` / `--no-prompt` | Skip all prompts. Required for non-TTY / CI invocations. |
|
|
34
|
-
| `--no-content` | Skip raw prompt/response/tool I/O capture. Spans still emit with timing, token usage, model name, and ids. Mirrored into both `config.allowConversationAccess` and `hooks.allowConversationAccess`. |
|
|
35
|
-
| `--no-trust` | Skip auto-adding the plugin id to `plugins.allow`. OpenClaw will keep printing `plugins.allow is empty` warnings until you add it manually. |
|
|
107
|
+
### Structural-only telemetry (no content capture)
|
|
36
108
|
|
|
37
|
-
|
|
109
|
+
If you want trace metadata (timings, token usage, model name, agent name, ids) without the prompt/response content, keep `hooks.allowConversationAccess` at `true` (so OpenClaw still dispatches events to us) and set only `config.allowConversationAccess` to `false`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' false
|
|
113
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Setting `hooks.allowConversationAccess=false` would block dispatch entirely — see [The two flags](#the-two-flags). The plugin still emits the full span tree; just the content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, tool args/results) are scrubbed. Each span carries a `latitude.captured.content: false` boolean so the gate state is visible in the Latitude UI.
|
|
38
117
|
|
|
39
118
|
## Uninstall
|
|
40
119
|
|
|
41
120
|
```bash
|
|
42
|
-
|
|
121
|
+
openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
|
|
43
122
|
```
|
|
44
123
|
|
|
45
|
-
|
|
124
|
+
OpenClaw removes the files, install record, plugin entry, and the `plugins.allow` entry. After that, restart the gateway:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
openclaw gateway restart
|
|
128
|
+
```
|
|
46
129
|
|
|
47
130
|
## What gets sent
|
|
48
131
|
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { arch, hostname, platform, release } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
3
|
//#region src/client.ts
|
|
7
4
|
async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs = 1e4 }) {
|
|
@@ -35,21 +32,32 @@ async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs
|
|
|
35
32
|
//#region src/config.ts
|
|
36
33
|
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
37
34
|
/**
|
|
38
|
-
* Build a `Config` from OpenClaw's per-plugin config bucket
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
35
|
+
* Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK
|
|
36
|
+
* passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)
|
|
37
|
+
* to the registration function — that's the only source.
|
|
38
|
+
*
|
|
39
|
+
* Earlier 0.0.x versions also fell back to environment variables when keys
|
|
40
|
+
* were missing from pluginConfig. That fallback is gone deliberately:
|
|
41
|
+
* OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis
|
|
42
|
+
* security scan that flags any runtime source combining environment-variable
|
|
43
|
+
* access with a network-send call (we have `fetch(` in postTraces). With
|
|
44
|
+
* the fallback our bundled runtime tripped the scanner. The installer
|
|
45
|
+
* writes credentials to `plugins.entries[id].config` anyway, so the
|
|
46
|
+
* fallback was polish-not-feature — its removal also gives a cleaner
|
|
47
|
+
* privacy story (the runtime can't pick up credentials the operator
|
|
48
|
+
* didn't put in openclaw.json).
|
|
49
|
+
*
|
|
50
|
+
* For dev-time testing with debug logs, set `config.debug = true` in
|
|
51
|
+
* openclaw.json directly.
|
|
44
52
|
*/
|
|
45
|
-
function loadConfig(pluginConfig = void 0
|
|
53
|
+
function loadConfig(pluginConfig = void 0) {
|
|
46
54
|
const fromOpts = pluginConfig ?? {};
|
|
47
|
-
const apiKey = pickString(fromOpts.apiKey) ??
|
|
48
|
-
const project = pickString(fromOpts.project) ??
|
|
49
|
-
const baseUrl = pickString(fromOpts.baseUrl) ??
|
|
50
|
-
const debug = pickBool(fromOpts.debug) ??
|
|
55
|
+
const apiKey = pickString(fromOpts.apiKey) ?? "";
|
|
56
|
+
const project = pickString(fromOpts.project) ?? "";
|
|
57
|
+
const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL;
|
|
58
|
+
const debug = pickBool(fromOpts.debug) ?? false;
|
|
51
59
|
const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false;
|
|
52
|
-
const explicitlyDisabled = pickBool(fromOpts.enabled) === false
|
|
60
|
+
const explicitlyDisabled = pickBool(fromOpts.enabled) === false;
|
|
53
61
|
return {
|
|
54
62
|
apiKey,
|
|
55
63
|
baseUrl,
|
|
@@ -77,7 +85,7 @@ function createLogger(debugEnabled) {
|
|
|
77
85
|
//#endregion
|
|
78
86
|
//#region src/otlp.ts
|
|
79
87
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
80
|
-
const SCOPE_VERSION =
|
|
88
|
+
const SCOPE_VERSION = "0.0.7";
|
|
81
89
|
/** Build an OTLP export request for a single completed agent run. */
|
|
82
90
|
function buildOtlpRequest(result, options) {
|
|
83
91
|
const spans = result.spans.map((span) => toOtlpSpan(span, options));
|
|
@@ -167,22 +175,6 @@ function safeJson$1(value) {
|
|
|
167
175
|
return "";
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Runtime-read package version, so OTLP `scope.version` and `service.version`
|
|
172
|
-
* always reflect what's actually installed. Read once at module load and
|
|
173
|
-
* cached. Falls back to `"unknown"` if the read fails.
|
|
174
|
-
*
|
|
175
|
-
* Same import.meta.url + ../package.json pattern used by `cli.ts` for the
|
|
176
|
-
* `--version` flag — single source of truth in package.json.
|
|
177
|
-
*/
|
|
178
|
-
function readScopeVersion() {
|
|
179
|
-
try {
|
|
180
|
-
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
181
|
-
return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
|
|
182
|
-
} catch {
|
|
183
|
-
return "unknown";
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
178
|
//#endregion
|
|
187
179
|
//#region src/messages.ts
|
|
188
180
|
const ALLOWED_ROLES = new Set([
|
|
@@ -437,6 +429,7 @@ var SpanBuilder = class {
|
|
|
437
429
|
attrs: {
|
|
438
430
|
...flattenCtx(ctx),
|
|
439
431
|
...latitudeAttrs(ctx),
|
|
432
|
+
...sessionAttrs(ctx),
|
|
440
433
|
"openclaw.run.id": runId,
|
|
441
434
|
"before_agent_start.prompt:gated": evt.prompt,
|
|
442
435
|
"before_agent_start.messages:gated": evt.messages ? normalizeMessages(evt.messages) : void 0
|
|
@@ -505,6 +498,7 @@ var SpanBuilder = class {
|
|
|
505
498
|
endMs: void 0,
|
|
506
499
|
attrs: {
|
|
507
500
|
...latitudeAttrs(ctx),
|
|
501
|
+
...sessionAttrs(ctx),
|
|
508
502
|
"openclaw.run.id": evt.runId,
|
|
509
503
|
"openclaw.call.id": evt.callId,
|
|
510
504
|
"gen_ai.system": evt.provider,
|
|
@@ -562,6 +556,7 @@ var SpanBuilder = class {
|
|
|
562
556
|
endMs: void 0,
|
|
563
557
|
attrs: {
|
|
564
558
|
...latitudeAttrs(ctx),
|
|
559
|
+
...sessionAttrs(ctx),
|
|
565
560
|
"openclaw.run.id": evt.runId,
|
|
566
561
|
"gen_ai.tool.name": evt.toolName,
|
|
567
562
|
"gen_ai.tool.call.id": toolCallId,
|
|
@@ -622,6 +617,7 @@ var SpanBuilder = class {
|
|
|
622
617
|
endMs: void 0,
|
|
623
618
|
attrs: {
|
|
624
619
|
...latitudeAttrs(ctx),
|
|
620
|
+
...sessionAttrs(ctx),
|
|
625
621
|
"openclaw.run.id": runId,
|
|
626
622
|
"openclaw.compaction.message_count.before": evt.messageCount,
|
|
627
623
|
"openclaw.compaction.session_file": evt.sessionFile,
|
|
@@ -663,6 +659,7 @@ var SpanBuilder = class {
|
|
|
663
659
|
endMs: void 0,
|
|
664
660
|
attrs: {
|
|
665
661
|
...latitudeAttrs(ctx),
|
|
662
|
+
...sessionAttrs(ctx),
|
|
666
663
|
"openclaw.parent.run.id": parentRunId,
|
|
667
664
|
"openclaw.run.id": evt.runId,
|
|
668
665
|
"openclaw.subagent.child_session_key": evt.childSessionKey,
|
|
@@ -819,6 +816,21 @@ function flattenCtx(ctx) {
|
|
|
819
816
|
};
|
|
820
817
|
}
|
|
821
818
|
/**
|
|
819
|
+
* Mirror OpenClaw's session id onto the OTEL-standard keys Latitude's
|
|
820
|
+
* resolver looks for. `gen_ai.session.id` and `session.id` are both in
|
|
821
|
+
* `sessionIdCandidates` (domain/spans/src/otlp/resolvers/identity.ts), so
|
|
822
|
+
* traces can be grouped by session in the Latitude UI without an
|
|
823
|
+
* openclaw-specific code path. Emitted on every span, not just `agent`,
|
|
824
|
+
* so child spans (model_call / tool_call / etc.) inherit the same grouping.
|
|
825
|
+
*/
|
|
826
|
+
function sessionAttrs(ctx) {
|
|
827
|
+
if (!ctx.sessionId) return {};
|
|
828
|
+
return {
|
|
829
|
+
"session.id": ctx.sessionId,
|
|
830
|
+
"gen_ai.session.id": ctx.sessionId
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
822
834
|
* Build `latitude.tags` and `latitude.metadata` attrs from the hook context.
|
|
823
835
|
* The OTLP encoder JSON-stringifies arrays/objects, which is the encoding
|
|
824
836
|
* Latitude's resolver expects:
|
|
@@ -937,19 +949,25 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
937
949
|
builder.onLlmOutput(evt, ctx);
|
|
938
950
|
}));
|
|
939
951
|
api.on("agent_end", wrap("agent_end", (evt, ctx) => {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
952
|
+
queueMicrotask(() => {
|
|
953
|
+
try {
|
|
954
|
+
const result = builder.onAgentEnd(evt, ctx);
|
|
955
|
+
if (!result) {
|
|
956
|
+
logger.debug("agent_end fired without a matching run in flight");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
opts.onEmit?.(result);
|
|
960
|
+
const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
|
|
961
|
+
postTraces({
|
|
962
|
+
baseUrl: config.baseUrl,
|
|
963
|
+
apiKey: config.apiKey,
|
|
964
|
+
project: config.project,
|
|
965
|
+
payload,
|
|
966
|
+
logger
|
|
967
|
+
});
|
|
968
|
+
} catch (err) {
|
|
969
|
+
logger.warn(`agent_end finalize failed: ${String(err)}`);
|
|
970
|
+
}
|
|
953
971
|
});
|
|
954
972
|
}));
|
|
955
973
|
}
|