From f8a02fdf22887748837f023e622c9ff4658f57e0 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Tue, 31 Mar 2026 13:07:56 +0300 Subject: [PATCH 1/5] feat: use database user ID as PostHog distinct_id instead of email --- .changeset/lazy-beds-lay.md | 6 +++ packages/cli/demo/json/es/example.json | 1 + packages/cli/demo/json/i18n.lock | 1 - packages/cli/src/cli/cmd/i18n.ts | 16 +++--- packages/cli/src/cli/cmd/run/_utils.ts | 13 +++-- packages/cli/src/cli/cmd/run/index.ts | 16 +++--- packages/cli/src/cli/cmd/status.ts | 36 +++++++------ packages/cli/src/cli/localizer/_types.ts | 1 + packages/cli/src/cli/localizer/lingodotdev.ts | 6 ++- packages/cli/src/cli/utils/observability.ts | 36 ++++++++----- packages/sdk/src/utils/observability.spec.ts | 12 ++--- packages/sdk/src/utils/observability.ts | 52 +++++++++++++------ 12 files changed, 122 insertions(+), 74 deletions(-) create mode 100644 .changeset/lazy-beds-lay.md diff --git a/.changeset/lazy-beds-lay.md b/.changeset/lazy-beds-lay.md new file mode 100644 index 000000000..a190f34e5 --- /dev/null +++ b/.changeset/lazy-beds-lay.md @@ -0,0 +1,6 @@ +--- +"lingo.dev": patch +"@lingo.dev/_sdk": patch +--- + +Migrate PostHog tracking identity from email to database user ID diff --git a/packages/cli/demo/json/es/example.json b/packages/cli/demo/json/es/example.json index 73205cb6a..262a5c2f0 100644 --- a/packages/cli/demo/json/es/example.json +++ b/packages/cli/demo/json/es/example.json @@ -33,6 +33,7 @@ } ], "locked_key_1": "This value is locked and should not be changed", + "ignored_key_1": "Este valor se ignora y no debe aparecer en los locales de destino", "preserved_key_1": "this value is preserved and should not be overwritten", "legal": { "preserved_nested": "this value is preserved and should not be overwritten" diff --git a/packages/cli/demo/json/i18n.lock b/packages/cli/demo/json/i18n.lock index afe6d3ef0..b0c8df40e 100644 --- a/packages/cli/demo/json/i18n.lock +++ b/packages/cli/demo/json/i18n.lock @@ -15,4 +15,3 @@ checksums: mixed_array/0: 001b5b003d96c133534f5907abffdf77 mixed_array/3/nested_message: 5f0782dfc5993e99890c0475bc295a30 ignored_key_1: 386cb6c3c496982b059671768905d66e - legal/preserved_nested: 1f22d279b6be8d261e125e54c5a6cb64 diff --git a/packages/cli/src/cli/cmd/i18n.ts b/packages/cli/src/cli/cmd/i18n.ts index 1214f7b66..3f828ad54 100644 --- a/packages/cli/src/cli/cmd/i18n.ts +++ b/packages/cli/src/cli/cmd/i18n.ts @@ -34,7 +34,7 @@ import externalEditor from "external-editor"; import updateGitignore from "../utils/update-gitignore"; import createProcessor from "../processor"; import { withExponentialBackoff } from "../utils/exp-backoff"; -import trackEvent from "../utils/observability"; +import trackEvent, { UserIdentity } from "../utils/observability"; import { createDeltaProcessor } from "../utils/delta"; export default new Command() @@ -135,7 +135,7 @@ export default new Command() } let hasErrors = false; - let email: string | null = null; + let userIdentity: UserIdentity = null; const errorDetails: ErrorDetail[] = []; try { ora.start("Loading configuration..."); @@ -151,15 +151,15 @@ export default new Command() const isByokMode = !!i18nConfig?.provider; if (isByokMode) { - email = null; + userIdentity = null; ora.succeed("Using external provider (BYOK mode)"); } else { const auth = await validateAuth(settings); - email = auth.email; + userIdentity = { email: auth.email, id: auth.id }; ora.succeed(`Authenticated as ${auth.email}`); } - await trackEvent(email, "cmd.i18n.start", { + await trackEvent(userIdentity, "cmd.i18n.start", { i18nConfig, flags, }); @@ -587,7 +587,7 @@ export default new Command() console.log(); if (!hasErrors) { ora.succeed("Localization completed."); - await trackEvent(email, "cmd.i18n.success", { + await trackEvent(userIdentity, "cmd.i18n.success", { i18nConfig: { sourceLocale: i18nConfig!.locale.source, targetLocales: i18nConfig!.locale.targets, @@ -602,7 +602,7 @@ export default new Command() } else { ora.warn("Localization completed with errors."); process.exitCode = 1; - await trackEvent(email, "cmd.i18n.error", { + await trackEvent(userIdentity, "cmd.i18n.error", { flags, ...aggregateErrorAnalytics( errorDetails, @@ -634,7 +634,7 @@ export default new Command() }; } - await trackEvent(email, "cmd.i18n.error", { + await trackEvent(userIdentity, "cmd.i18n.error", { flags, errorType, errorName: error.name || "Error", diff --git a/packages/cli/src/cli/cmd/run/_utils.ts b/packages/cli/src/cli/cmd/run/_utils.ts index 5afbd287d..8e91eb9c7 100644 --- a/packages/cli/src/cli/cmd/run/_utils.ts +++ b/packages/cli/src/cli/cmd/run/_utils.ts @@ -1,12 +1,13 @@ import { CmdRunContext } from "./_types"; +import { UserIdentity } from "../../utils/observability"; /** - * Determines the user's email for tracking purposes. + * Determines the user's identity for tracking purposes. * Returns null if using BYOK mode or if authentication fails. */ -export async function determineEmail( +export async function determineUserIdentity( ctx: CmdRunContext, -): Promise { +): Promise { const isByokMode = !!ctx.config?.provider; if (isByokMode) { @@ -14,7 +15,11 @@ export async function determineEmail( } else { try { const authStatus = await ctx.localizer?.checkAuth(); - return authStatus?.username || null; + if (!authStatus?.username || !authStatus?.userId) return null; + return { + email: authStatus.username, + id: authStatus.userId, + }; } catch { return null; } diff --git a/packages/cli/src/cli/cmd/run/index.ts b/packages/cli/src/cli/cmd/run/index.ts index 050b32e6a..7b28ecd9f 100644 --- a/packages/cli/src/cli/cmd/run/index.ts +++ b/packages/cli/src/cli/cmd/run/index.ts @@ -18,8 +18,8 @@ import { pauseIfDebug, renderSummary, } from "../../utils/ui"; -import trackEvent from "../../utils/observability"; -import { determineEmail } from "./_utils"; +import trackEvent, { UserIdentity } from "../../utils/observability"; +import { determineUserIdentity } from "./_utils"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -124,7 +124,7 @@ export default new Command() "Enable pseudo-localization mode: automatically pseudo-translates all extracted strings with accented characters and visual markers without calling any external API. Useful for testing UI internationalization readiness", ) .action(async (args) => { - let email: string | null = null; + let userIdentity: UserIdentity = null; try { const ctx: CmdRunContext = { flags: flagsSchema.parse(args), @@ -143,9 +143,9 @@ export default new Command() await setup(ctx); - email = await determineEmail(ctx); + userIdentity = await determineUserIdentity(ctx); - await trackEvent(email, "cmd.run.start", { + await trackEvent(userIdentity, "cmd.run.start", { config: ctx.config, flags: ctx.flags, }); @@ -176,16 +176,16 @@ export default new Command() await watch(ctx); } - await trackEvent(email, "cmd.run.success", { + await trackEvent(userIdentity, "cmd.run.success", { config: ctx.config, flags: ctx.flags, }); await new Promise((resolve) => setTimeout(resolve, 50)); } catch (error: any) { - await trackEvent(email, "cmd.run.error", { + await trackEvent(userIdentity, "cmd.run.error", { flags: args, error: error.message, - authenticated: !!email, + authenticated: !!userIdentity, }); await new Promise((resolve) => setTimeout(resolve, 50)); // Play sad sound if sound flag is enabled diff --git a/packages/cli/src/cli/cmd/status.ts b/packages/cli/src/cli/cmd/status.ts index abc441ab0..692eb46d1 100644 --- a/packages/cli/src/cli/cmd/status.ts +++ b/packages/cli/src/cli/cmd/status.ts @@ -18,7 +18,7 @@ import { getBuckets } from "../utils/buckets"; import chalk from "chalk"; import Table from "cli-table3"; import { createDeltaProcessor } from "../utils/delta"; -import trackEvent from "../utils/observability"; +import trackEvent, { UserIdentity } from "../utils/observability"; import { minimatch } from "minimatch"; import { exitGracefully } from "../utils/exit-gracefully"; @@ -63,7 +63,7 @@ export default new Command() .action(async function (options) { const ora = Ora(); const flags = parseFlags(options); - let email: string | null = null; + let userIdentity: UserIdentity = null; try { ora.start("Loading configuration..."); @@ -76,7 +76,7 @@ export default new Command() ora.start("Checking authentication status..."); const auth = await tryAuthenticate(settings); if (auth) { - email = auth.email; + userIdentity = { email: auth.email, id: auth.id }; ora.succeed(`Authenticated as ${auth.email}`); } else { ora.info( @@ -92,7 +92,7 @@ export default new Command() ora.succeed("Localization configuration is valid"); // Track event with or without authentication - trackEvent(email, "cmd.status.start", { + trackEvent(userIdentity, "cmd.status.start", { i18nConfig, flags, }); @@ -356,10 +356,11 @@ export default new Command() )} (${completeKeys.length}/${totalKeysInFile} keys)`, ); } else { - const message = `[${sourceLocale} -> ${targetLocale}] ${parseFloat(completionPercent) > 50 + const message = `[${sourceLocale} -> ${targetLocale}] ${ + parseFloat(completionPercent) > 50 ? chalk.yellow(`${completionPercent}% complete`) : chalk.red(`${completionPercent}% complete`) - } (${completeKeys.length}/${totalKeysInFile} keys)`; + } (${completeKeys.length}/${totalKeysInFile} keys)`; bucketOra.succeed(message); @@ -369,7 +370,8 @@ export default new Command() ` ${chalk.red(`Missing:`)} ${missingKeys.length} keys, ~${wordsToTranslate} words`, ); console.log( - ` ${chalk.red(`Missing:`)} ${missingKeys.length + ` ${chalk.red(`Missing:`)} ${ + missingKeys.length } keys, ~${wordsToTranslate} words`, ); console.log( @@ -382,7 +384,8 @@ export default new Command() } if (updatedKeys.length > 0) { console.log( - ` ${chalk.yellow(`Updated:`)} ${updatedKeys.length + ` ${chalk.yellow(`Updated:`)} ${ + updatedKeys.length } keys that changed in source`, ); } @@ -536,7 +539,8 @@ export default new Command() console.log(chalk.bold(`\n• ${path}:`)); console.log( - ` ${stats.sourceKeys + ` ${ + stats.sourceKeys } source keys, ~${stats.wordCount.toLocaleString()} source words`, ); @@ -603,14 +607,16 @@ export default new Command() if (missingLanguages.length > 0) { console.log( - `• ${chalk.yellow(missingLanguages.join(", "))} ${missingLanguages.length === 1 ? "has" : "have" + `• ${chalk.yellow(missingLanguages.join(", "))} ${ + missingLanguages.length === 1 ? "has" : "have" } no translations yet`, ); } if (completeLanguages.length > 0) { console.log( - `• ${chalk.green(completeLanguages.join(", "))} ${completeLanguages.length === 1 ? "is" : "are" + `• ${chalk.green(completeLanguages.join(", "))} ${ + completeLanguages.length === 1 ? "is" : "are" } completely translated`, ); } @@ -624,22 +630,22 @@ export default new Command() } // Track successful completion - trackEvent(email, "cmd.status.success", { + trackEvent(userIdentity, "cmd.status.success", { i18nConfig, flags, totalSourceKeyCount, languageStats, totalWordsToTranslate, - authenticated: !!email, + authenticated: !!userIdentity, }); await new Promise((resolve) => setTimeout(resolve, 50)); exitGracefully(); } catch (error: any) { ora.fail(error.message); - trackEvent(email, "cmd.status.error", { + trackEvent(userIdentity, "cmd.status.error", { flags, error: error.message, - authenticated: !!email, + authenticated: !!userIdentity, }); await new Promise((resolve) => setTimeout(resolve, 50)); process.exit(1); diff --git a/packages/cli/src/cli/localizer/_types.ts b/packages/cli/src/cli/localizer/_types.ts index 8cfe8f431..da613fba6 100644 --- a/packages/cli/src/cli/localizer/_types.ts +++ b/packages/cli/src/cli/localizer/_types.ts @@ -21,6 +21,7 @@ export interface ILocalizer { checkAuth: () => Promise<{ authenticated: boolean; username?: string; + userId?: string; error?: string; }>; validateSettings?: () => Promise<{ valid: boolean; error?: string }>; diff --git a/packages/cli/src/cli/localizer/lingodotdev.ts b/packages/cli/src/cli/localizer/lingodotdev.ts index 18527b19d..dc22a6d3c 100644 --- a/packages/cli/src/cli/localizer/lingodotdev.ts +++ b/packages/cli/src/cli/localizer/lingodotdev.ts @@ -46,7 +46,11 @@ export default function createLingoDotDevLocalizer( "Invalid API key. Run `lingo.dev login` or check your LINGO_API_KEY.", }; } - return { authenticated: true, username: response.email }; + return { + authenticated: true, + username: response.email, + userId: response.id, + }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/cli/src/cli/utils/observability.ts b/packages/cli/src/cli/utils/observability.ts index 3f954750b..888405d3c 100644 --- a/packages/cli/src/cli/utils/observability.ts +++ b/packages/cli/src/cli/utils/observability.ts @@ -1,7 +1,6 @@ import pkg from "node-machine-id"; const { machineIdSync } = pkg; import https from "https"; -import crypto from "crypto"; import { getOrgId } from "./org-id"; const POSTHOG_API_KEY = "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk"; @@ -10,17 +9,22 @@ const POSTHOG_PATH = "/i/v0/e/"; const REQUEST_TIMEOUT_MS = 3000; const TRACKING_VERSION = "2.0"; -function determineDistinctId(email: string | null | undefined): { +export type UserIdentity = { + email: string; + id: string; +} | null; + +function determineDistinctId(user: UserIdentity): { distinct_id: string; distinct_id_source: string; org_id: string | null; } { const orgId = getOrgId(); - if (email) { + if (user) { return { - distinct_id: email, - distinct_id_source: "email", + distinct_id: user.id, + distinct_id_source: "database_id", org_id: orgId, }; } @@ -47,7 +51,7 @@ function determineDistinctId(email: string | null | undefined): { } export default function trackEvent( - email: string | null | undefined, + user: UserIdentity, event: string, properties?: Record, ): void { @@ -57,7 +61,7 @@ export default function trackEvent( setImmediate(() => { try { - const identityInfo = determineDistinctId(email); + const identityInfo = determineDistinctId(user); if (process.env.DEBUG === "true") { console.log( @@ -71,7 +75,10 @@ export default function trackEvent( distinct_id: identityInfo.distinct_id, properties: { ...properties, - $set: { ...(properties?.$set || {}), ...(email ? { email } : {}) }, + $set: { + ...(properties?.$set || {}), + ...(user ? { email: user.email } : {}), + }, $lib: "lingo.dev-cli", $lib_version: process.env.npm_package_version || "unknown", tracking_version: TRACKING_VERSION, @@ -112,15 +119,14 @@ export default function trackEvent( req.write(payload); req.end(); - // TODO: remove after 2026-03-25 — temporary alias to merge old hashed distinct_ids with new raw email - if (email) { - const hashedEmail = crypto.createHash("sha256").update(email).digest("hex"); + // TODO: remove after 2026-04-30 — temporary alias to merge old email-based distinct_ids with database user ID + if (user) { const aliasData = JSON.stringify({ api_key: POSTHOG_API_KEY, event: "$create_alias", - distinct_id: email, + distinct_id: user.id, properties: { - alias: hashedEmail, + alias: user.email, }, timestamp: new Date().toISOString(), }); @@ -136,7 +142,9 @@ export default function trackEvent( aliasReq.on("error", () => {}); aliasReq.write(aliasData); aliasReq.end(); - setTimeout(() => { if (!aliasReq.destroyed) aliasReq.destroy(); }, REQUEST_TIMEOUT_MS); + setTimeout(() => { + if (!aliasReq.destroyed) aliasReq.destroy(); + }, REQUEST_TIMEOUT_MS); } setTimeout(() => { diff --git a/packages/sdk/src/utils/observability.spec.ts b/packages/sdk/src/utils/observability.spec.ts index 507fa29a6..c1fd466f7 100644 --- a/packages/sdk/src/utils/observability.spec.ts +++ b/packages/sdk/src/utils/observability.spec.ts @@ -4,7 +4,7 @@ import { trackEvent, _resetIdentityCache } from "./observability"; const capture = vi.fn(async () => undefined); const shutdown = vi.fn(async () => undefined); const PostHogMock = vi.fn(function (_key: string, _cfg: any) { - return { capture, shutdown }; + return { alias: vi.fn(), capture, shutdown }; }); vi.mock("posthog-node", () => ({ PostHog: PostHogMock })); @@ -29,7 +29,7 @@ describe("trackEvent", () => { expect(PostHogMock).not.toHaveBeenCalled(); }); - it("captures event with email identity when whoami succeeds", async () => { + it("captures event with database user ID when whoami succeeds", async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ email: "user@test.com", id: "123" }), @@ -43,11 +43,11 @@ describe("trackEvent", () => { expect(capture).toHaveBeenCalledWith( expect.objectContaining({ - distinctId: "user@test.com", + distinctId: "123", event: "sdk.localize.start", properties: expect.objectContaining({ method: "localizeText", - distinct_id_source: "email", + distinct_id_source: "database_id", tracking_version: "1.0", sdk_package: "@lingo.dev/_sdk", }), @@ -75,10 +75,10 @@ describe("trackEvent", () => { ); }); - it("falls back to API key hash when whoami returns no email", async () => { + it("falls back to API key hash when whoami returns no id", async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, - json: async () => ({}), + json: async () => ({ email: "user@test.com" }), }) as any; trackEvent("my-api-key", "https://test.api", "sdk.localize.start", {}); diff --git a/packages/sdk/src/utils/observability.ts b/packages/sdk/src/utils/observability.ts index 09eb4667e..ed898fe86 100644 --- a/packages/sdk/src/utils/observability.ts +++ b/packages/sdk/src/utils/observability.ts @@ -9,7 +9,10 @@ type IdentityInfo = { distinct_id_source: string; }; -const identityCache = new Map(); +const identityCache = new Map< + string, + { identity: IdentityInfo; email?: string } +>(); export function trackEvent( apiKey: string, @@ -36,11 +39,11 @@ async function resolveIdentityAndCapture( event: string, properties?: Record, ) { - const identityInfo = await getDistinctId(apiKey, apiUrl); + const { identity, email } = await getDistinctId(apiKey, apiUrl); if (process.env.DEBUG === "true") { console.log( - `[Tracking] Event: ${event}, ID: ${identityInfo.distinct_id}, Source: ${identityInfo.distinct_id_source}`, + `[Tracking] Event: ${event}, ID: ${identity.distinct_id}, Source: ${identity.distinct_id_source}`, ); } @@ -52,23 +55,32 @@ async function resolveIdentityAndCapture( }); await posthog.capture({ - distinctId: identityInfo.distinct_id, + distinctId: identity.distinct_id, event, properties: { ...properties, + $set: email ? { email } : {}, tracking_version: TRACKING_VERSION, sdk_package: SDK_PACKAGE, - distinct_id_source: identityInfo.distinct_id_source, + distinct_id_source: identity.distinct_id_source, }, }); + // TODO: remove after 2026-04-30 — temporary alias to merge old email-based distinct_ids with database user ID + if (email) { + await posthog.alias({ + distinctId: identity.distinct_id, + alias: email, + }); + } + await posthog.shutdown(); } async function getDistinctId( apiKey: string, apiUrl: string, -): Promise { +): Promise<{ identity: IdentityInfo; email?: string }> { const cached = identityCache.get(apiKey); if (cached) return cached; @@ -80,15 +92,19 @@ async function getDistinctId( "Content-Type": "application/json", }, }); + if (res.ok) { const payload = await res.json(); - if (payload?.email) { - const identity: IdentityInfo = { - distinct_id: payload.email, - distinct_id_source: "email", + if (payload?.id) { + const result = { + identity: { + distinct_id: payload.id, + distinct_id_source: "database_id", + }, + email: payload.email || undefined, }; - identityCache.set(apiKey, identity); - return identity; + identityCache.set(apiKey, result); + return result; } } } catch { @@ -96,12 +112,14 @@ async function getDistinctId( } const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 16); - const identity: IdentityInfo = { - distinct_id: `apikey-${hash}`, - distinct_id_source: "api_key_hash", + const result = { + identity: { + distinct_id: `apikey-${hash}`, + distinct_id_source: "api_key_hash", + }, }; - identityCache.set(apiKey, identity); - return identity; + identityCache.set(apiKey, result); + return result; } export function _resetIdentityCache() { From d9de4d50ef2437985fcd541ced46c6fb35d23234 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Tue, 31 Mar 2026 13:10:51 +0300 Subject: [PATCH 2/5] fix: revert jsom demo changes --- packages/cli/demo/json/es/example.json | 1 - packages/cli/demo/json/i18n.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/cli/demo/json/es/example.json b/packages/cli/demo/json/es/example.json index 262a5c2f0..73205cb6a 100644 --- a/packages/cli/demo/json/es/example.json +++ b/packages/cli/demo/json/es/example.json @@ -33,7 +33,6 @@ } ], "locked_key_1": "This value is locked and should not be changed", - "ignored_key_1": "Este valor se ignora y no debe aparecer en los locales de destino", "preserved_key_1": "this value is preserved and should not be overwritten", "legal": { "preserved_nested": "this value is preserved and should not be overwritten" diff --git a/packages/cli/demo/json/i18n.lock b/packages/cli/demo/json/i18n.lock index b0c8df40e..c394f1891 100644 --- a/packages/cli/demo/json/i18n.lock +++ b/packages/cli/demo/json/i18n.lock @@ -14,4 +14,3 @@ checksums: config/theme/primary: 7535a3779d6934ea8ecf18f5cb5b93fd mixed_array/0: 001b5b003d96c133534f5907abffdf77 mixed_array/3/nested_message: 5f0782dfc5993e99890c0475bc295a30 - ignored_key_1: 386cb6c3c496982b059671768905d66e From ba1b35a7f51919bf0684cb8f6877791e64dc9871 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Tue, 31 Mar 2026 13:11:54 +0300 Subject: [PATCH 3/5] fix: revert jsom demo changes --- packages/cli/demo/json/i18n.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/demo/json/i18n.lock b/packages/cli/demo/json/i18n.lock index c394f1891..afe6d3ef0 100644 --- a/packages/cli/demo/json/i18n.lock +++ b/packages/cli/demo/json/i18n.lock @@ -14,3 +14,5 @@ checksums: config/theme/primary: 7535a3779d6934ea8ecf18f5cb5b93fd mixed_array/0: 001b5b003d96c133534f5907abffdf77 mixed_array/3/nested_message: 5f0782dfc5993e99890c0475bc295a30 + ignored_key_1: 386cb6c3c496982b059671768905d66e + legal/preserved_nested: 1f22d279b6be8d261e125e54c5a6cb64 From f547cc97592ebe750fb9c970e8fb490cf7c8a8de Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Tue, 31 Mar 2026 14:41:54 +0300 Subject: [PATCH 4/5] fix: don't cache failed /users/me identity --- packages/sdk/src/utils/observability.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/utils/observability.ts b/packages/sdk/src/utils/observability.ts index ed898fe86..07c44de99 100644 --- a/packages/sdk/src/utils/observability.ts +++ b/packages/sdk/src/utils/observability.ts @@ -111,15 +111,14 @@ async function getDistinctId( // Fall through to API key hash } + // Don't cache the fallback — a transient /users/me failure should not poison the cache for the entire process lifetime const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 16); - const result = { + return { identity: { distinct_id: `apikey-${hash}`, distinct_id_source: "api_key_hash", }, }; - identityCache.set(apiKey, result); - return result; } export function _resetIdentityCache() { From 9fc2c76938d36d807e746396792f92639a22ce96 Mon Sep 17 00:00:00 2001 From: AndreyHirsa Date: Tue, 31 Mar 2026 15:09:05 +0300 Subject: [PATCH 5/5] fix: prevent override and ensure posthog shutdown on error --- packages/sdk/src/utils/observability.ts | 38 +++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/sdk/src/utils/observability.ts b/packages/sdk/src/utils/observability.ts index 07c44de99..5ac0c9e12 100644 --- a/packages/sdk/src/utils/observability.ts +++ b/packages/sdk/src/utils/observability.ts @@ -54,27 +54,29 @@ async function resolveIdentityAndCapture( flushInterval: 0, }); - await posthog.capture({ - distinctId: identity.distinct_id, - event, - properties: { - ...properties, - $set: email ? { email } : {}, - tracking_version: TRACKING_VERSION, - sdk_package: SDK_PACKAGE, - distinct_id_source: identity.distinct_id_source, - }, - }); - - // TODO: remove after 2026-04-30 — temporary alias to merge old email-based distinct_ids with database user ID - if (email) { - await posthog.alias({ + try { + await posthog.capture({ distinctId: identity.distinct_id, - alias: email, + event, + properties: { + ...properties, + $set: { ...(properties?.$set || {}), ...(email ? { email } : {}) }, + tracking_version: TRACKING_VERSION, + sdk_package: SDK_PACKAGE, + distinct_id_source: identity.distinct_id_source, + }, }); - } - await posthog.shutdown(); + // TODO: remove after 2026-04-30 — temporary alias to merge old email-based distinct_ids with database user ID + if (email) { + await posthog.alias({ + distinctId: identity.distinct_id, + alias: email, + }); + } + } finally { + await posthog.shutdown(); + } } async function getDistinctId(