Telemetry
Browse docs
GuidesUpdated July 30, 2026Reviewed by the Telemetry editorial and product teams5 min read

Use this doc with your coding agent

Open a focused prompt pack for Claude Code, Codex, Cursor, or another coding agent, then adapt it to the workflow covered here.

On this page
  1. When this pattern fits
  2. Define the client contract
  3. Implement the application-owned endpoint
  4. Apply four server-side controls
  5. Authenticate
  6. Allowlist
  7. Rate limit
  8. Add trusted context
  9. Choose the mobile delivery behavior
  10. Query and verify the result
  11. Failure modes to plan for
  12. Primary references

Secure mobile app telemetry through a server proxy

Mobile applications are distributed to devices you do not control. Anything compiled into a React Native bundle, Swift app, Kotlin app, or Flutter binary can eventually be inspected. Do not ship a Telemetry API key in the app, a remote-config value delivered to the app, or a client-readable environment file.

Instead, send a small event to an authenticated endpoint owned by your application. That endpoint validates a fixed contract, adds trusted server context, and forwards the event with a server-side key.

Conceptual mobile telemetry flow from a mobile app through an application API with authentication, allowlisting, and rate limiting before Telemetry

The mobile app never receives the Telemetry ingestion key. Your API owns authentication, validation, sampling, and forwarding.

When this pattern fits

Use a mobile proxy for product milestones, bounded performance measurements, sync outcomes, controlled error categories, and release-quality signals. Examples include:

  • mobile_sync_completed after a local-to-server sync reaches a terminal outcome.
  • mobile_screen_ready for a small set of named screens and a measured duration.
  • mobile_purchase_flow_completed after the server confirms the durable outcome.
  • mobile_api_request_completed for sampled, categorized dependency results.

This is not a replacement for crash symbolication, device logs, distributed traces, or an exact audit ledger. Mobile delivery is affected by connectivity, background execution limits, app termination, consent, and client clock quality. Keep specialist diagnostics in their specialist systems and send only the outcome needed for SQL analysis.

Define the client contract

The client should choose from a versioned list of event names and fields. It should not choose a Telemetry table name, send arbitrary property bags, or forward exception messages.

{
  "event_name": "mobile_sync_completed",
  "event_version": 1,
  "event_id": "0196f37e-6e83-7b75-9d04-6b8283b35f74",
  "occurred_at": "2026-07-30T18:42:11.150Z",
  "status": "success",
  "duration_ms": 842,
  "item_count": 12,
  "network_type": "wifi"
}

Keep dimensions bounded. Prefer a controlled screen_name over a raw route, an error_type enum over an exception string, and a coarse network_type over network identifiers. Do not include access tokens, device advertising identifiers, contact data, message contents, file paths, clipboard data, free-form search text, or raw URLs.

event_id supports retry deduplication. occurred_at records the client observation, but the proxy should also add a server-received timestamp. Use the server timestamp for freshness and ingestion monitoring because a device clock can be wrong.

Implement the application-owned endpoint

The following TypeScript sketch shows the boundary. Adapt authentication and rate limiting to the framework already used by your API.

const allowedEvents = {
  mobile_sync_completed: {
    statuses: new Set(["success", "failed", "cancelled"]),
    maximumDurationMs: 300_000,
    maximumItemCount: 10_000,
  },
} as const;

export async function postMobileTelemetry(request: Request) {
  const actor = await authenticateApplicationRequest(request);
  if (!actor) return new Response("unauthorized", { status: 401 });

  await enforceRateLimit({
    accountId: actor.accountId,
    deviceSessionId: actor.deviceSessionId,
  });

  const body = await readBoundedJson(request, { maximumBytes: 4096 });
  const policy = allowedEvents[body.event_name as keyof typeof allowedEvents];

  if (
    !policy ||
    body.event_version !== 1 ||
    !isUuid(body.event_id) ||
    !policy.statuses.has(body.status) ||
    !isIntegerInRange(body.duration_ms, 0, policy.maximumDurationMs) ||
    !isIntegerInRange(body.item_count, 0, policy.maximumItemCount)
  ) {
    return new Response("invalid event", { status: 422 });
  }

  await telemetry.log("mobile_sync_completed", {
    event_id: body.event_id,
    event_version: 1,
    occurred_at: parseBoundedClientTimestamp(body.occurred_at),
    received_at: new Date().toISOString(),
    status: body.status,
    duration_ms: body.duration_ms,
    item_count: body.item_count,
    network_type: normalizeNetworkType(body.network_type),
    account_id: actor.accountId,
    app_platform: actor.platform,
    app_version: actor.appVersion,
    environment: process.env.APP_ENV ?? "development",
  });

  return new Response(null, { status: 202 });
}

The API decides the Telemetry event name. It derives identity, platform, environment, and any authorization context from trusted server state rather than accepting those values from the client. If the endpoint supports more than one event, give every event an independent schema and test fixture.

Apply four server-side controls

Authenticate

Require the same signed application session or installation credential used by the rest of your API. CORS is not a mobile security boundary, and a custom header alone does not prove who sent a request.

Allowlist

Reject unknown event names, fields, enum values, oversized strings, invalid numbers, future timestamps, and bodies above a small size limit. The allowlist is both a privacy control and a cardinality control.

Rate limit

Limit by authenticated account and an appropriate installation or session identifier. Also impose a global ceiling. Return a normal application error without retrying indefinitely when a client exceeds the limit.

Add trusted context

The proxy should add the account identifier, server receive time, environment, and verified application version when those values are available. If an account identifier is sensitive in your context, pseudonymize it consistently before collection and document who can reverse the mapping.

Choose the mobile delivery behavior

For React Native and Flutter, use the platform HTTP client already responsible for authenticated API requests. For native iOS and Android, use the same URLSession or HTTP stack used by the app. The endpoint and event contract should remain identical across platforms.

Keep a small bounded queue when offline delivery matters. Cap both the item count and age, discard events that have exceeded the documented window, and use exponential backoff with jitter. Do not let analytics retries delay the product action. Preserve the same event_id across attempts so the server can deduplicate.

Some outcomes are better emitted by the server. A purchase, subscription change, access-policy decision, or completed import becomes authoritative only after the backend commits it. Let the server emit those outcomes directly instead of trusting a client assertion.

Query and verify the result

Send one synthetic success, failure, cancellation, invalid payload, unauthenticated request, rate-limited burst, offline retry, and duplicate event_id. Then inspect a bounded sample:

SELECT
  received_at,
  event_id,
  app_platform,
  app_version,
  status,
  duration_ms,
  item_count
FROM mobile_sync_completed
ORDER BY received_at DESC
LIMIT 50;

Before rollout, verify:

  1. The shipped application binary and JavaScript bundle contain no Telemetry API key.
  2. Unknown events and fields are rejected rather than silently forwarded.
  3. Raw exception text, URLs, user input, credentials, and device identifiers are absent.
  4. Retries keep one event_id, and duplicate delivery does not inflate outcome counts.
  5. Dashboards display sample count beside rates and percentiles.
  6. Consent, retention, deletion, and account-removal behavior match the application policy.

Failure modes to plan for

Treat the proxy as best-effort observability unless the event is part of a separately designed durable business workflow. A Telemetry timeout should normally be logged and bounded without failing the mobile product action. Monitor proxy rejection rate, forwarding failures, queue age, and event freshness so a silent instrumentation break does not look like a product-usage drop.

Do not automatically accept arbitrary client events to “make debugging easier.” That turns the endpoint into an unaudited data collection surface. Add a new versioned field or event only after reviewing its purpose, type, cardinality, privacy classification, and deletion requirements.

Primary references

Related product capability

Capture stable event names, typed fields, and privacy-reviewed context.

Ownership and technical references

The Telemetry editorial team owns this explanation; the product team reviews behavior, examples, and boundaries.

Review the editorial standard