@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 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** on PATH. Older versions are detected up-front and the installer aborts with an upgrade message.
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
- The installer prompts for your Latitude API key and project slug, then:
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
- 1. Verifies your OpenClaw version (aborts on `< 2026.4.25`).
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
- Traces show up at `https://console.latitude.so/projects/<your-slug>` once the gateway restarts.
97
+ For staging or local-dev, also set `baseUrl`:
24
98
 
25
- ### Install flags
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
- | Flag | What it does |
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
- Re-running `install` is idempotent credentials/baseUrl are overwritten from prompts, but hand-edited `enabled`, `debug`, and `allowConversationAccess` values in `openclaw.json` are preserved unless you pass the corresponding flag.
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
- npx -y @latitude-data/openclaw-telemetry uninstall
121
+ openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
43
122
  ```
44
123
 
45
- Shows a plan, asks for confirmation, then runs `openclaw plugins uninstall @latitude-data/openclaw-telemetry --force` (which removes files, the install record, the plugin entry, and the `plugins.allow` entry). Defensive cleanup follows for any leftover keys, with a backup at `openclaw.json.latitude-bak`.
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 plus environment
39
- * variables. The plugin SDK passes `api.pluginConfig` (the user's
40
- * `plugins.entries[id].config` block) to the registration function — that's
41
- * the primary source. Env vars are kept as a fallback so existing deployments
42
- * with `LATITUDE_*` already exported in the gateway environment keep working,
43
- * and so that `LATITUDE_DEBUG=1` can be flipped without editing openclaw.json.
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, env = process.env) {
53
+ function loadConfig(pluginConfig = void 0) {
46
54
  const fromOpts = pluginConfig ?? {};
47
- const apiKey = pickString(fromOpts.apiKey) ?? env.LATITUDE_API_KEY ?? "";
48
- const project = pickString(fromOpts.project) ?? env.LATITUDE_PROJECT ?? "";
49
- const baseUrl = pickString(fromOpts.baseUrl) ?? env.LATITUDE_BASE_URL ?? DEFAULT_BASE_URL;
50
- const debug = pickBool(fromOpts.debug) ?? env.LATITUDE_DEBUG === "1";
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 || (env.LATITUDE_OPENCLAW_ENABLED ?? "1") === "0";
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 = readScopeVersion();
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
- const result = builder.onAgentEnd(evt, ctx);
941
- if (!result) {
942
- logger.debug("agent_end fired without a matching run in flight");
943
- return;
944
- }
945
- opts.onEmit?.(result);
946
- const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess });
947
- postTraces({
948
- baseUrl: config.baseUrl,
949
- apiKey: config.apiKey,
950
- project: config.project,
951
- payload,
952
- logger
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
  }