LOG_LEVEL=DEBUG was writing the user's plaintext email to stderr/log
handlers, contradicting the PR's no-plaintext-email-on-disk stance.
The exception object alone is enough debug signal.
Telemetry is best-effort by contract — every PostHog hiccup or
network blip would print red error lines on user stderr otherwise.
Python uses logger.debug for the same paths; this restores parity.
Keeps console.error only at sites that surface a real user-facing
failure (e.g., MemoryClient init ping failed).
Previously captureIdentify returned true only on response.ok, so a
stable PostHog 4xx (api-key revocation, schema mismatch) or any 5xx
caused every MemoryClient init to re-fire $identify forever. PostHog
person-merge is idempotent, so it is safe to mark aliased on any
response received. Only genuine network failures leave the marker
absent so the next init naturally retries.
Also downgrades the failure log lines to console.debug and stops
echoing the PostHog response body to stderr.
The default posthog client enqueues events on a background thread, so
capture() returning a UUID does NOT mean the event was delivered. After
PR #5040 this caused mark_aliased to persist a marker even when PostHog
rejected the payload, permanently severing the anon->email merge.
Use a separate lazy-singleton Posthog(sync_mode=True) client for
$identify only. Check result is not None to handle both posthog<7
(raises on failure) and posthog>=7 (@no_throw catches and returns
None). Keeps the async client for everything else.
Also drops the email from the failure-path debug log.
Followup to ec5baaba: the previous commit dropped the unset/empty
telemetryId fallback. Restoring it preserves behavioral parity with
pre-PR code in case telemetryId is ever externally mutated to a
falsy value (the type permits "" even if normal flow never sets it).
Stop overwriting a stable vector-store UUID with the shared SDK
user_id. Existing OSS users were having their telemetry identity
silently swapped on next init, orphaning every prior PostHog event
tagged with the old id (the same regression class as the bug PR
#5040 set out to fix).
The shared SDK id is now adopted only when the vector store reports
"anonymous" / "anonymous-supabase" (fresh-install signals).
Review pass on the PostHog identity-stitching fix:
- Gate _maybe_alias_anon_to_email and _maybeAliasAnonToEmail on telemetry
being enabled. Previous version still read ~/.mem0/config.json and
persisted telemetry.aliased_to even when MEM0_TELEMETRY=false, which
meant re-enabling telemetry later would skip identity merging forever.
Python checks client_telemetry.posthog; TS exports a new
isTelemetryEnabled() getter from telemetry.ts.
- Trim capture_identify properties to {$anon_distinct_id, client_source}.
The full platform metadata duplicated capture_event's payload and is
not needed for $identify; PostHog enriches the person profile from
subsequent events anyway.
- Flatten config.ts: drop the four-helper structure (loadNodeModules /
configPath / loadRawConfig / read+write) into one getNodeFs() + a
shared loadConfig(). Same behaviour, ~30 fewer lines, easier to read.
- Drop the tautological Mem0AnonIds shape test and the
void createMockFetch hack at the end of the TS test file.
- Add tests for the new telemetry-disabled gate (Python + TS).
Logic traced end-to-end: 114 Python tests + 590 TS tests pass. Ruff and
Prettier clean. Zero new typecheck errors.
The Python sync/async MemoryClient and the TS MemoryClient were silently
switching their PostHog distinct_id from an anonymous on-disk UUID to the
user's email post-ping, without sending the merge instruction. PostHog
ended up tracking each user as 2-3 disconnected personas (OSS UUID, CLI
anon UUID, email), and only ~4 $create_alias events fired across ~300K
users. The Python CLI does this correctly today; this change ports the
same pattern to MemoryClient.
- mem0/memory/setup.py: make setup_config() idempotent so it backfills a
top-level user_id for CLI-first users (the CLI writes
telemetry.anonymous_id, not user_id, which left OSS telemetry silently
dropped). Add read_anon_ids() and mark_aliased() helpers.
- mem0/memory/telemetry.py: add AnonymousTelemetry.capture_identify() that
posts a PostHog $identify event with $anon_distinct_id. Add "$identify"
to _LIFECYCLE_EVENTS so it bypasses the 90% sampling drop.
- mem0/client/main.py: at the end of __init__ for both MemoryClient and
AsyncMemoryClient, fire $identify for each unaliased anon ID and
persist telemetry.aliased_to so the alias only happens once per
(anon_id, email) pair, ever. All telemetry calls swallow exceptions.
- mem0-ts/src/client/telemetry.ts: mirror with captureIdentify().
- mem0-ts/src/client/config.ts (new): node-only readMem0AnonIds() and
markMem0Aliased() using async dynamic imports of fs/path/os, gated on
process.versions.node so browsers no-op cleanly.
- mem0-ts/src/client/mem0.ts: call _maybeAliasAnonToEmail() inside
_initializeClient after ping resolves the email.
Tests cover the matrix (OSS-only, CLI-only, both, already-aliased) plus
failure modes (malformed JSON, read-only fs, posthog errors, browser
env). Telemetry never raises.