@yagni-app/code-staging 1.0.7-staging.1270.1 → 1.0.8-staging.1273.1

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
@@ -203,7 +203,14 @@ prefix instead of `claude_code`:
203
203
  - **Log events:** `user_prompt`, `assistant_response`, `api_request`,
204
204
  `api_error`, `tool_result`, `tool_decision`, `permission_mode_changed`
205
205
  (as `event.name`, body `yagni_code.<name>`), with `session.id`,
206
- `organization.id`, `terminal.type`, and a per-prompt `prompt.id`.
206
+ `organization.id`, `user.email`, `terminal.type`, and a per-prompt
207
+ `prompt.id`.
208
+
209
+ Every signal carries `organization.id` (the workspace) and `user.email` (the
210
+ signed-in developer's account email, learned from the backend at session
211
+ start) so a collector can attribute sessions and spend per person.
212
+ `OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false` drops the email from all three
213
+ signals. Prompt, response, and tool content never export.
207
214
 
208
215
  Nothing is exported unless you configure an endpoint. Enable it one of three
209
216
  ways (first match wins):
@@ -127,6 +127,12 @@ export interface ContextBrief {
127
127
  * The opt-in mining beat fires ONLY on an explicit 0.
128
128
  */
129
129
  repoDecisionCount?: number;
130
+ /**
131
+ * The signed-in developer's account email (additive; absent on older
132
+ * backends). Feeds the OTel export's `user.email` attribute when the
133
+ * launcher did not already forward one.
134
+ */
135
+ userEmail?: string;
130
136
  }
131
137
  /**
132
138
  * Attribution headers for the model proxy (YAG-471). On the SERVER side these
@@ -3,6 +3,7 @@ import { type SpendResponse } from "./costHud.js";
3
3
  import { runInitPass as defaultRunInitPass } from "./initPass.js";
4
4
  import { startMcp as defaultStartMcp } from "./mcp/startup.js";
5
5
  import { type GuardianGateEvent } from "./permission/gate.js";
6
+ import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
6
7
  import { type PluginCliRunner } from "./plugins/panel.js";
7
8
  import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
8
9
  import { type TokenProvider } from "./tokenProvider.js";
@@ -67,6 +68,11 @@ export interface RegisterYagniDeps {
67
68
  * fresh-workspace first-run and is skipped otherwise, without a network or disk.
68
69
  */
69
70
  runInitPass?: typeof defaultRunInitPass;
71
+ /**
72
+ * OTel export registration. Injectable so tests assert the boot handoff of
73
+ * the /context `userEmail` into the telemetry identity without an SDK.
74
+ */
75
+ registerTelemetry?: typeof defaultRegisterTelemetry;
70
76
  /** The workspace id the token is bound to (keys the one-time init marker). */
71
77
  getWorkspaceId?: () => string | undefined;
72
78
  /** Has the init pass already run once for this workspace? Injectable (no disk in tests). */
@@ -37,7 +37,7 @@ import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
37
37
  import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
38
38
  import { registerSandbox } from "./sandbox/session.js";
39
39
  import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
40
- import { registerTelemetry } from "./telemetry/register.js";
40
+ import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
41
41
  import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
42
42
  import { loadPermissionRules } from "./permissionRules/loadConfig.js";
43
43
  import { registerPluginPanel } from "./plugins/panel.js";
@@ -421,7 +421,7 @@ export async function registerYagni(pi, deps = {}) {
421
421
  // OTel export (traces, metrics, log events; Claude Code parity). A no-op
422
422
  // unless the launcher's gate set YAGNI_OTEL_EXPORT=1 — see the CLI's
423
423
  // otel.ts for the sources and src/telemetry for the exporter.
424
- const telemetry = registerTelemetry(pi, { env });
424
+ const telemetry = (deps.registerTelemetry ?? defaultRegisterTelemetry)(pi, { env });
425
425
  {
426
426
  let previousMode = modeHolder.get();
427
427
  modeHolder.onSet((m) => {
@@ -840,6 +840,29 @@ export async function registerYagni(pi, deps = {}) {
840
840
  contextBrief = undefined;
841
841
  }
842
842
  }
843
+ // The developer's account email for the OTel export (`user.email`), so a
844
+ // collector can attribute sessions per person. Set before session_start
845
+ // fires, so even the session.count metric carries it. No-op when export is
846
+ // off or the launcher already forwarded YAGNI_USER_EMAIL.
847
+ const bootUserEmail = typeof briefResult?.userEmail === "string" ? briefResult.userEmail : undefined;
848
+ telemetry.setUserEmail(bootUserEmail);
849
+ if (telemetry.enabled && !bootUserEmail && !telemetry.config.identity.userEmail) {
850
+ // Attribution would otherwise degrade silently: leave a trail in the local
851
+ // error sink so "why is user.email missing in the collector" is answerable.
852
+ // Info, not warn: one line per session start, and an account with no
853
+ // email hits it by design. The fields name each way the email can be
854
+ // absent — the boot fetch failed or lacked the field, and whether the
855
+ // launcher's YAGNI_USER_EMAIL was unset or forwarded blank.
856
+ logEvent({
857
+ source: "telemetry",
858
+ level: "info",
859
+ event: "user_email_missing",
860
+ fields: {
861
+ contextFetch: briefResult === null ? "failed_or_skipped" : "no_email_field",
862
+ launcherEmail: env.YAGNI_USER_EMAIL === undefined ? "unset" : "blank",
863
+ },
864
+ });
865
+ }
843
866
  // M1 ambient judgment recall: after a `read`, append the recorded judgment for
844
867
  // that path to the tool result (result modification is supported on pi's
845
868
  // `tool_result` event — verified against 0.80.2). No-op when the corpus is thin
@@ -37,7 +37,11 @@ export interface SignalConfig {
37
37
  export interface TelemetryIdentity {
38
38
  /** Bound workspace id (YAGNI_WORKSPACE_ID) → `organization.id`. */
39
39
  organizationId?: string;
40
- /** The signed-in user's email when the launcher forwards it → `user.email`. */
40
+ /**
41
+ * The signed-in user's email → `user.email`. From `YAGNI_USER_EMAIL` when
42
+ * the launcher forwards it; otherwise learned from the /context boot fetch
43
+ * (`TelemetryHandle.setUserEmail`). Gated by `includeAccountId`.
44
+ */
41
45
  userEmail?: string;
42
46
  /** Shipping CLI version (YAGNI_CODE_VERSION) → `app.version`. */
43
47
  appVersion?: string;
@@ -27,6 +27,13 @@ export interface TelemetryHandle {
27
27
  toolDecision(input: ToolDecisionInput): void;
28
28
  /** The mode holder calls this on every /mode (or shift-tab) change. */
29
29
  permissionModeChanged(fromMode: string, toMode: string): void;
30
+ /**
31
+ * The entry calls this with the account email the /context boot fetch
32
+ * returned. A launcher-forwarded `YAGNI_USER_EMAIL` wins; otherwise every
33
+ * span, metric, and event from here on carries `user.email` (still subject
34
+ * to the `includeAccountId` gate).
35
+ */
36
+ setUserEmail(email: string | undefined): void;
30
37
  /** Test/introspection seam: the live tracker once the SDK is up. */
31
38
  readonly tracker: SessionTelemetry | null;
32
39
  }
@@ -27,6 +27,7 @@ const NOOP_HANDLE = (config) => ({
27
27
  config,
28
28
  toolDecision: () => { },
29
29
  permissionModeChanged: () => { },
30
+ setUserEmail: () => { },
30
31
  tracker: null,
31
32
  });
32
33
  export function registerTelemetry(pi, deps = {}) {
@@ -187,6 +188,20 @@ export function registerTelemetry(pi, deps = {}) {
187
188
  },
188
189
  toolDecision: guard("tool_decision", (input) => tracker?.toolDecision(input)),
189
190
  permissionModeChanged: guard("permission_mode_changed", (from, to) => tracker?.permissionModeChanged(from, to)),
191
+ // Precedence, lowest to highest: this call (the /context boot fetch) <
192
+ // the launcher's YAGNI_USER_EMAIL, which resolveTelemetryConfig already
193
+ // placed on `identity`. So an identity that is set is never overwritten,
194
+ // whichever writer set it; a new writer must slot into this order
195
+ // explicitly rather than rely on call timing. The tracker reads
196
+ // `config.identity` on every attribute set, so this applies whether the
197
+ // SDK is already up or still lazy-loading.
198
+ setUserEmail: (email) => {
199
+ if (config.identity.userEmail)
200
+ return;
201
+ const trimmed = typeof email === "string" ? email.trim() : "";
202
+ if (trimmed)
203
+ config.identity.userEmail = trimmed;
204
+ },
190
205
  };
191
206
  }
192
207
  //# sourceMappingURL=register.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.0.7-staging.1270.1",
3
+ "version": "1.0.8-staging.1273.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "7148d4d0a99f32505ec662e0bbd209279306b4b9"
61
+ "yagniSourceSha": "bb2c4a103e2db69e8dcc6eec98c7343ddefca05c"
62
62
  }