Moss.DeviceId (moss v1.1.0)

Copy Markdown

Stable, per-device identifier sourcing for MOS-14 "better tracking" parity.

This module's ONLY job is to source a stable, persisted, per-device id and hand it to the closed core, which owns the actual /telemetry POST + buffer

  • 3s flush. The SDK never touches telemetry transport.

Contract (see the canonical MOS-14 device-id spec):

  • Elixir is a server/CLI platform with no OS-blessed vendor id, so the id is a generated UUIDv4 persisted on first use (spec R1.2). There is no Apple identifierForVendor equivalent here.
  • The id is an opaque string handed through unchanged (R1.3). A blank persisted value is treated as absent and regenerated (R1.4).
  • Persistence (R2.2, file-platform class): a plaintext file named exactly .moss-device-id. When a client cache dir is known it lives at <cachePath>/.moss-device-id; otherwise it falls back to a single per-user dir so a device counts once toward Monthly Active Devices (R2.3). The per-user dir mirrors the naming intent dev.moss.sdk / account device_id from the Keychain platforms by using a stable moss user-cache dir plus a .moss fallback.
  • The store is device-scoped and MUST NOT sync/migrate to another device (R2). A user-cache / home dir is non-synced by construction.
  • Persistence failure must never break the client: on any error we fall back to a fresh ephemeral (non-persisted) UUID (R2.4).
  • MOSS_DISABLE_TELEMETRY (truthy set, trimmed + lowercased) is honored, checked before the memo fast-path, at runtime: when disabled we source no id, do no store I/O, and hand nothing to the core (R4).

Memoization (R3) and "apply once" tracking are held per-client in the GenServer/struct state that calls this module; this module is a pure resolver plus a best-effort apply helper.

In moss-sdks-internal the native setter EXISTS: the core exposes IndexManager::set_device_id and SessionIndex::set_device_id, and the Elixir NIF surfaces them as MossCore.Nif.manager_set_device_id/2 and session_set_device_id/2. So callers pass a REAL apply_fun closure and the id is actually handed to the core (fully functional, not inert). apply/2 still degrades to a no-op success when telemetry is disabled or no setter is wired (R5.4), but that is the fallback path, not the norm.

Summary

Types

Per-client device-id memo state: the resolved id and whether it was applied to the core.

Functions

Best-effort push of id to the core via apply_fun, which should call the binding's setter NIF (setter mechanism, R5.2). Never raises (R5.3).

Resolve the device id (once, shared via state) and push it to the core via apply_fun. No-op once applied or when telemetry is disabled (R3.2).

Per-user fallback directory for the device-id file, used when no cache_path is available (e.g. the session API, which has no cache dir of its own).

A fresh, empty per-client memo state.

Resolve the stable per-device id persisted at <dir>/.moss-device-id.

Resolve the client's device id ONCE and memoize it on state, so every telemetry surface a client touches (the IndexManager, ManageClient, and any sessions) reports the same id — one device, one id (R3.1, R5.5).

True when usage telemetry is disabled via MOSS_DISABLE_TELEMETRY.

Types

state()

@type state() :: %{optional(:id) => String.t() | nil, applied: boolean()}

Per-client device-id memo state: the resolved id and whether it was applied to the core.

Functions

apply(apply_fun, id)

@spec apply((String.t() -> term()) | nil, String.t()) :: boolean()

Best-effort push of id to the core via apply_fun, which should call the binding's setter NIF (setter mechanism, R5.2). Never raises (R5.3).

Returns true when the id is now settled — on success, OR when the setter is not available in this build (telemetry disabled / no setter wired: terminal, nothing to retry, R5.4). Returns false only when the apply raised, so the caller may retry later (R3.3).

apply_fun is an arity-1 function returning one of:

  • :ok / {:ok, _} -> success
  • :unsupported -> setter not present in this build (terminal success)
  • {:error, _} / raises -> transient failure (retry)

In moss-sdks-internal the callers pass a REAL closure that invokes MossCore.Nif.manager_set_device_id/2 / session_set_device_id/2, so this actually hands the id to the core. Passing nil (no setter wired) is still accepted and treated as :unsupported for the degrade path (R5.4).

apply_once(state, apply_fun, cache_path)

@spec apply_once(state(), (String.t() -> term()) | nil, String.t() | nil) :: state()

Resolve the device id (once, shared via state) and push it to the core via apply_fun. No-op once applied or when telemetry is disabled (R3.2).

On a transient apply failure state.applied stays false so the next call retries rather than permanently suppressing the id (R3.3). Returns the new state.

default_dir()

@spec default_dir() :: String.t()

Per-user fallback directory for the device-id file, used when no cache_path is available (e.g. the session API, which has no cache dir of its own).

Resolves to a stable, per-user, non-synced location so a device's id is the same across processes and surfaces — it counts once toward Monthly Active Devices (R2.3). Uses $XDG_CACHE_HOME/moss when that env var is set, falling back to <home>/.moss where home = $HOME -> %USERPROFILE% -> OS home, with blank values skipped so a blank $HOME does not resolve into the CWD (R2.2).

new_state()

@spec new_state() :: state()

A fresh, empty per-client memo state.

resolve(dir)

@spec resolve(String.t()) :: String.t() | nil

Resolve the stable per-device id persisted at <dir>/.moss-device-id.

Reads an existing non-blank UUID, or generates and writes one. Returns nil when telemetry is disabled (no store I/O). On any filesystem error, returns a fresh ephemeral UUID (NOT persisted) so telemetry can still attribute within this run — device-id persistence must never break a real operation (R2.4).

resolve_client(state, cache_path)

@spec resolve_client(state(), String.t() | nil) :: {String.t() | nil, state()}

Resolve the client's device id ONCE and memoize it on state, so every telemetry surface a client touches (the IndexManager, ManageClient, and any sessions) reports the same id — one device, one id (R3.1, R5.5).

Persists under cache_path when a non-blank one is given, otherwise under default_dir/0. Returns {id_or_nil, new_state}. Returns nil (without memoizing) when telemetry is disabled, and — because the disabled-check runs before the memo fast-path — a runtime opt-out takes effect immediately even after an id was memoized (R3.3, R4.2).

telemetry_disabled?()

@spec telemetry_disabled?() :: boolean()

True when usage telemetry is disabled via MOSS_DISABLE_TELEMETRY.

Truthy set (trimmed + lowercased): ["1", "true", "yes", "on"]. Checked at runtime so toggling the env var mid-process takes effect immediately (R4.2).