From 331291c0f7b2f376db080764899badd0a7fee3bd Mon Sep 17 00:00:00 2001 From: SvenAlHamad Date: Fri, 5 Jun 2026 11:37:19 +0200 Subject: [PATCH] fix: add distinctId support to the Node client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Node WTS client always read or minted `user.id` in ~/.webiny/config and had no way to use an externally-owned machine id. The Webiny admin and the website install/finish alias both key off the canonical @webiny/global-config top-level `id`, while the CLI used a separate `user.id` UUID — fragmenting funnels across CLI/admin/website. Add an optional `distinctId` to NodeClientConfig (mirroring the web client's fixedId). When provided it is used as-is and the config file is left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- __tests__/node.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/node.ts | 17 +++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/__tests__/node.test.ts b/__tests__/node.test.ts index bd344b8..477a808 100644 --- a/__tests__/node.test.ts +++ b/__tests__/node.test.ts @@ -69,6 +69,46 @@ test("WTS node client reuses existing user.id from config", async () => { expect(body.distinct_id).toBe(existingId); }); +test("WTS node client uses a provided distinctId as-is", async () => { + const wts = new WTS({ + source: "cli", + configPath, + distinctId: "global-config-id-abc", + apiUrl: "https://t.example.com" + }); + wts.track("cli-create-webiny-project-start"); + + await new Promise(r => setTimeout(r, 10)); + const body = JSON.parse(capturedRequests[0]!.init.body as string); + expect(body.distinct_id).toBe("global-config-id-abc"); +}); + +test("WTS node client leaves the config file untouched when distinctId is provided", async () => { + const wts = new WTS({ source: "cli", configPath, distinctId: "global-config-id-abc" }); + wts.track("cli-create-webiny-project-start"); + + await new Promise(r => setTimeout(r, 10)); + // No machine id should be minted into ~/.webiny/config when an id is injected. + expect(existsSync(configPath)).toBe(false); +}); + +test("WTS node client prefers distinctId over an existing user.id in config", async () => { + mkdirSync(join(tmpDir, ".webiny"), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ user: { id: "existing-user-id" } })); + + const wts = new WTS({ + source: "cli", + configPath, + distinctId: "global-config-id-abc", + apiUrl: "https://t.example.com" + }); + wts.track("cli-create-webiny-project-start"); + + await new Promise(r => setTimeout(r, 10)); + const body = JSON.parse(capturedRequests[0]!.init.body as string); + expect(body.distinct_id).toBe("global-config-id-abc"); +}); + test("WTS node client honors WEBINY_TELEMETRY=false env var", async () => { process.env.WEBINY_TELEMETRY = "false"; diff --git a/src/node.ts b/src/node.ts index 0682a81..727763b 100644 --- a/src/node.ts +++ b/src/node.ts @@ -19,6 +19,14 @@ const OPT_OUT_ENV = "WEBINY_TELEMETRY"; export interface NodeClientConfig extends ClientConfig { /** Override the path to the Webiny config file. Defaults to ~/.webiny/config. */ configPath?: string; + /** + * Force a specific distinct_id instead of reading/minting the machine id in + * the Webiny config file. When provided, it's used as-is and the config file + * is left untouched. Webiny passes the canonical `@webiny/global-config` id + * here so CLI, admin, and website events share one identity; without it the + * client falls back to its own `user.id` field, which is a different UUID. + */ + distinctId?: string; /** Number of retry attempts for transient HTTP errors. Defaults to 3. */ retries?: number; /** Base delay in ms between retries (exponential backoff). Defaults to 200. */ @@ -28,12 +36,17 @@ export interface NodeClientConfig extends ClientConfig { class NodeIdentity implements Identity { private path: string; private cached: string | null = null; + private fixedId: string | null; - constructor(configPath?: string) { + constructor(configPath?: string, fixedId?: string) { this.path = configPath ?? join(homedir(), CONFIG_DIR, CONFIG_FILE); + this.fixedId = fixedId ?? null; } getDistinctId(): string | null { + if (this.fixedId) { + return this.fixedId; + } if (this.cached) { return this.cached; } @@ -120,7 +133,7 @@ export class WTS extends TelemetryClient { constructor(config: NodeClientConfig) { super( config, - new NodeIdentity(config.configPath), + new NodeIdentity(config.configPath, config.distinctId), new NodeTransport(config.retries ?? 3, config.retryDelay ?? 200) ); }