# Cancel a Scheduled Notification Source: https://docs.lerix.dev/api-reference/cancel-notification POST https://api.lerix.dev/v1/{project-id}/notifications/{id}/cancel Stop a scheduled notification before it's delivered. Cancels a notification that hasn't been delivered yet — only valid while its status is `scheduled`. Deleting a notification's row wouldn't be enough on its own since the delivery is asynchronous; this marks it `canceled` so the scheduled-delivery job skips it when its time comes. Calling this on a notification that's already `sent`, `failed`, `unsent`, or already `canceled` returns `CANNOT_CANCEL_SENT_NOTIFICATION`. > [!NOTE] > > See the [API introduction](/api-reference/introduction) to set up your base URL and private key before making requests. ## Parameters **project-id** `string` *required* Your project ID from **Project Settings** in the [dashboard](https://app.lerix.dev). **id** `string` *required* The `notificationId` returned when the notification was scheduled (see [Send a notification](/api-reference/push-notifications) and its `sentAt` parameter). **apiKey** `string` Deprecated alternative to the `lerix-key` header, still accepted. ## Response ```json 201 Created theme={"dark"} { "message": "Scheduled notification canceled" } ``` On failure, the response follows the shape documented in [Error & success codes](/developers/errors-responses). # Introduction Source: https://docs.lerix.dev/api-reference/introduction Authenticate and send requests to the Lerix REST API. ## Base URL All API requests are sent to your project endpoint: ``` https://api.lerix.dev/v1/:project-id ``` Replace `:project-id` with your project ID from the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. **Example:** ``` https://api.lerix.dev/v1/my-project-id ``` ## Authentication Before you call the API, create a **private key** in **Project Settings** in the [dashboard](https://app.lerix.dev). Send the private key on every request in the `lerix-key` header: ```bash theme={"dark"} curl https://api.lerix.dev/v1/my-project-id/endpoint \ -H "lerix-key: " ``` > [!WARNING] > > Keep your private key secret. Do not commit it to source control, expose it in client-side code, or share it publicly. ## Next steps > **[Push Notifications](/api-reference/push-notifications)** > > Send push notifications through the REST API. > **[Update a Notification](/api-reference/update-notification)** > > Change a notification's title and body. > **[Unsend a Notification](/api-reference/unsend-notification)** > > Remove an already-delivered notification from devices. > **[Cancel a Scheduled Notification](/api-reference/cancel-notification)** > > Stop a scheduled notification before it goes out. > **[Get Delivery Status](/api-reference/notification-deliveries)** > > Read per-recipient delivery outcomes. > **[Error & success codes](/developers/errors-responses)** > > Reference common API response codes. # Get Delivery Status Source: https://docs.lerix.dev/api-reference/notification-deliveries GET https://api.lerix.dev/v1/{project-id}/notifications/{id}/deliveries Read the per-recipient delivery outcome for a notification. Returns one entry per recipient the notification was actually sent to, with the real outcome APNs/FCM reported — not just the notification's overall status. Useful for seeing exactly which devices received it and why any individual delivery failed. > [!NOTE] > > See the [API introduction](/api-reference/introduction) to set up your base URL and private key before making requests. ## Parameters **project-id** `string` *required* Your project ID from **Project Settings** in the [dashboard](https://app.lerix.dev). **id** `string` *required* The `notificationId` returned when the notification was sent. **apiKey** `string` Deprecated alternative to the `lerix-key` header, still accepted. ## Response ```json 200 OK theme={"dark"} [ { "id": "3f1e2b0a-1c3f-4b3e-9c3f-4b3e9c3f4b3e", "platform": "ios", "status": "sent", "error": null, "deviceToken": "a1b2c3d4e5f6...", "createdAt": "2026-09-01T10:00:03.000Z" }, { "id": "9c3f4b3e-1c3f-4b3e-9c3f-4b3e9c3f4b3e", "platform": "android", "status": "failed", "error": "messaging/registration-token-not-registered", "deviceToken": null, "createdAt": "2026-09-01T10:00:03.000Z" } ] ``` **platform** `string` `ios`, `android`, `web`, `macos` or `windows`. A device registered from a platform Lerix can't deliver to yet (for example Linux) keeps its own value here. **status** `string` `sent` or `failed`, the real outcome APNs/FCM reported for this recipient. **error** `string | null` The failure reason when `status` is `failed`; `null` otherwise. This is either the provider's own reason, or one of these Lerix codes in brackets followed by a message: | Code | Meaning | | - | - | | `PLATFORM_NOT_CONFIGURED` | The device's platform has no push keys in this project yet (for example an iOS device but no APNs key). Add them in **Notifications → Settings**. Other devices in the same send are unaffected. | | `UNSUPPORTED_PLATFORM` | Lerix has no push provider for this device's platform (for example Linux). | Windows deliveries can also report the WNS codes listed in [Windows troubleshooting](/frameworks/flutter/windows#troubleshooting). Example: `[PLATFORM_NOT_CONFIGURED] Apple keys not configured`. **deviceToken** `string | null` The device token this delivery targeted. `null` if the token was automatically removed after being reported permanently invalid — the delivery record is kept even after its token is gone. An empty array means nothing has been attempted yet — for example, the notification is still `scheduled`, or was `canceled` before it was due. On failure, the response follows the shape documented in [Error & success codes](/developers/errors-responses). # Push Notifications Source: https://docs.lerix.dev/api-reference/push-notifications POST https://api.lerix.dev/v1/{project-id}/notifications/send Send push notifications to devices, to your own users on every device they use, or to a topic through the Lerix REST API. Send a push notification to one of three targets: * **`deviceTokens`**: specific registered devices. * **`externalUserIds`**: your own user IDs. Lerix delivers to every device each user signed in on (iOS, Android, web and macOS) in one request. Your apps link a device to a user with `setUser`; see [Identify users](/modules/users-identity). * **`topic`** (with `sendByTopic: true`): every device subscribed to a topic. Use one target per request. `externalUserIds` can't be combined with `deviceTokens` or `sendByTopic`: that request is rejected with `NOTIFICATION_TARGET_INVALID`. > [!NOTE] > > See the [API introduction](/api-reference/introduction) to set up your base URL and private key before making requests. ## Parameters **project-id** `string` *required* Your project ID from **Project Settings** in the [dashboard](https://app.lerix.dev). **title** `string` *required* Notification title shown to the user. **body** `string` *required* Notification message body. **deviceTokens** `string[]` One or more device tokens to deliver the notification to. One of the three targets: don't combine it with `externalUserIds` or `sendByTopic`. **externalUserIds** `string[]` Your own user IDs, the same values your apps pass to `setUser` after login. Lerix finds every device currently linked to each ID, across all the platforms in this project, and delivers to all of them. Up to 1000 IDs per request, each 1 to 255 characters. One of the three targets: don't combine it with `deviceTokens` or `sendByTopic`. An ID with no linked device isn't an error (the user may not have installed the app yet). It comes back in `unknownUserIds` in the response. When the notification is scheduled with `sentAt`, devices are looked up again at send time, so a device that signs in after you schedule still receives it. **sendByTopic** `boolean` (default: false) Set to `true` to deliver to every device subscribed to `topic`. One of the three targets: don't combine it with `deviceTokens` or `externalUserIds`. Devices subscribe to a topic from your app using the SDK. A topic is created automatically the first time a device subscribes to it, or you can create one manually from the **Topics** tab in the dashboard. **topic** `string` The topic key to broadcast to. Required when `sendByTopic` is `true`, ignored otherwise. **sound** `string` Name of a custom notification sound. The file itself must already be bundled inside your app project — Lerix only passes the filename through to APNs/FCM, it doesn't host or upload sound files. Leave empty to use the device's default notification sound. **iOS**: the sound file's name with its extension (e.g. `sound.mp3`; `.caf`/`.wav` also supported). **Android**: the resource name **without** an extension, matching a file under `res/raw/` (e.g. `sound` for a file at `res/raw/sound.mp3`). * Supported formats: `.wav`, `.mp3`, `.ogg`. * Keep the filename lowercase — some tooling/devices ignore uppercase letters in resource names. * Keep the file small and under \~30 seconds; large files may not play on some devices. * If the named file isn't found, the notification falls back to the device's default sound rather than failing. * A given sound is locked in the first time it's used on a device — Android fixes a notification channel's sound at creation and never lets it change afterward, so switching to a different `sound` value later creates a new channel rather than updating the old one. **Web**: not supported — the Web Notifications API has no cross-browser way to set a custom sound, so this parameter is ignored for web recipients. **imageUrl** `string` URL of an image to show as a rich image in the notification. Lerix downloads it — this is a normal `https://` URL, not a file you need to bundle in your app (unlike `sound`). **Android**: works automatically, rendered as a `BigPictureStyle` notification. **iOS**: requires the app to have a Notification Service Extension configured (a one-time setup step — see the [Flutter push notifications guide](/frameworks/flutter/push-notifications#sending-an-image)). Without it, `imageUrl` is silently ignored on iOS; the notification still sends and displays normally, just without the image. Sending this field automatically sets APNs' `mutable-content` flag, which is what triggers the extension to run. **Web**: works automatically, no extra setup — the service worker renders it as the notification's icon/image directly. **metadata** `object` Arbitrary custom data to attach to the notification. Any JSON object; omit for no metadata. Delivered to the device alongside `title`/`body` and readable from the SDK as `payload.metadata` in `setOnNotificationReceived`/`setOnNotificationTapped`. **projectSlug** `string` Defaults to the project in the URL (`/v1/:project-id/...`); only needed for older integrations. **apiKey** `string` Deprecated alternative to the `lerix-key` header, still accepted. **source** `string` (default: API) Where the notification was sent from. **sentAt** `string` An ISO 8601 timestamp to schedule delivery for later instead of sending immediately. Omit it (or pass a time in the past) to send right away. A scheduled notification can be edited or canceled before it goes out — see [Update a notification](/api-reference/update-notification) and [Cancel a scheduled notification](/api-reference/cancel-notification). > [!WARNING] > > Never expose your private key in client-side code or public repositories. Send API requests from a trusted backend only. ## Response ```json 201 Created theme={"dark"} { "success": true, "message": "Notification sent successfully", "notificationId": "b6b1a2b0-9c3f-4b3e-9c3f-4b3e9c3f4b3e" } ``` Save `notificationId`. It's what you pass to the other notification endpoints (update, unsend, cancel, and delivery status). When you send to `externalUserIds`, the response also says who was reached: ```json 201 Created theme={"dark"} { "success": true, "message": "Notification sent successfully", "notificationId": "b6b1a2b0-9c3f-4b3e-9c3f-4b3e9c3f4b3e", "matchedUsers": 1, "matchedDevices": 3, "unknownUserIds": ["user_456"] } ``` **matchedUsers** `number` How many of the requested IDs have at least one linked device. **matchedDevices** `number` How many devices the notification was sent to, across every platform. **unknownUserIds** `string[]` Requested IDs with no linked device. Nothing was sent for them. A device on a platform whose push keys aren't configured yet doesn't block the others: it gets a failed delivery with the `PLATFORM_NOT_CONFIGURED` code, visible in [Get delivery status](/api-reference/notification-deliveries). ## Errors | HTTP status | `error` | Meaning | | - | - | - | | 400 | `NOTIFICATION_TARGET_INVALID` | The request combined `externalUserIds` with `deviceTokens` or `sendByTopic` | Every other failure follows the shape and codes documented in [Error & success codes](/developers/errors-responses). # Unsend a Notification Source: https://docs.lerix.dev/api-reference/unsend-notification POST https://api.lerix.dev/v1/{project-id}/notifications/{id}/unsend Remove an already-delivered push notification from recipients' devices. Tells every device that confirmed receipt of a notification to remove it from its notification tray, using a silent follow-up push. Only valid for notifications with status `sent` — one that's `scheduled` was never delivered (use [Cancel a scheduled notification](/api-reference/cancel-notification) instead), and `failed`/`canceled`/`unsent` ones have nothing left on a device to remove. The notification stays in your history; its status becomes `unsent`. This only reaches out to the device — it never deletes anything from history. > [!NOTE] > > See the [API introduction](/api-reference/introduction) to set up your base URL and private key before making requests. > [!WARNING] > > This requires the recipient's app to be running a client SDK version that > supports unsend (it needs the notification-removal handler wired into > `AppDelegate.swift` on iOS). Devices on an older app version can't have a > notification recalled, no matter what this endpoint does server-side. ## Parameters **project-id** `string` *required* Your project ID from **Project Settings** in the [dashboard](https://app.lerix.dev). **id** `string` *required* The `notificationId` returned when the notification was sent. **apiKey** `string` Deprecated alternative to the `lerix-key` header, still accepted. ## Response ```json 201 Created theme={"dark"} { "message": "Notification unsent" } ``` On failure, the response follows the shape documented in [Error & success codes](/developers/errors-responses). # Update a Notification Source: https://docs.lerix.dev/api-reference/update-notification POST https://api.lerix.dev/v1/{project-id}/notifications/{id}/update Change a notification's title and body after it's been sent or scheduled. Edits a notification's `title` and `body`, using the `notificationId` returned from [Send a notification](/api-reference/push-notifications). > [!NOTE] > > See the [API introduction](/api-reference/introduction) to set up your base URL and private key before making requests. ## How this behaves by status | Current status | What happens | | - | - | | `scheduled` | Edited in place — nothing is sent yet, so the new `title`/`body` is simply what gets delivered once it's actually due. | | `sent`, `failed`, `unsent` | Already had its delivery attempt, so "updating" means **redelivering immediately** with the new content to the current recipients (for a topic, whoever is subscribed right now — not a stale snapshot). | | `canceled` | Can't be updated — returns `CANNOT_UPDATE_CANCELED_NOTIFICATION`. | ## Parameters **project-id** `string` *required* Your project ID from **Project Settings** in the [dashboard](https://app.lerix.dev). **id** `string` *required* The `notificationId` returned when the notification was sent. **apiKey** `string` Deprecated alternative to the `lerix-key` header, still accepted. **title** `string` *required* New notification title. **body** `string` *required* New notification message body. ## Response ```json 201 Created theme={"dark"} { "id": "b6b1a2b0-9c3f-4b3e-9c3f-4b3e9c3f4b3e", "title": "Hello", "body": "You have a new message.", "status": "scheduled", "updatedAt": "2026-09-01T10:00:00.000Z" // ...remaining notification fields } ``` On failure, the response follows the shape documented in [Error & success codes](/developers/errors-responses). # The Dashboard Source: https://docs.lerix.dev/dashboard A tour of the Lerix dashboard — organizations, projects, and where each module lives. ## Overview Everything you configure and monitor lives in the [Lerix dashboard](https://app.lerix.dev), organized into two levels: **organizations** and **projects**. ``` Organization ├── Members & roles ├── Billing & subscription └── Projects ├── Issues (Error Tracking) ├── Requests (Request Tracing) ├── Notifications ├── Feedbacks (Feedback and NPS surveys) ├── Analytics and Events ├── Release control └── Project settings ``` ## Organizations An organization is the top-level container for your team. From an organization you manage: * **Members** — invite teammates and manage their roles. * **Billing** — your current plan, usage against plan limits, payment history, and upgrading/downgrading. If you have a redeem code, you apply it here when creating a new organization to activate its plan. * **Projects** — every project that belongs to this organization. ## Projects A project is one product you're monitoring. It can include several platforms: when you create it, pick more than one framework card (for example Flutter and Next.js) and every app of that product shares the project. Each project has its own **API key**, so data from different products never mixes, and one push send can reach a user on every device they use (see [Identify users](/modules/users-identity)). Some teams also create one project per environment. ### Project settings * **General**: the project's name and its platforms. Add a platform here when you ship the product somewhere new (the first one is the primary platform). * **API** — your project's API key and slug, used to initialize the SDK (`apiKey`, `projectId`) and to authenticate REST API requests. * **Members** — who on your organization has access to this specific project. * **Integrations** — connect external tools (e.g. GitHub, for linking issues to repositories). * **Webhooks & Slack**: post new feedback, NPS detractors and new issues to Slack or your own endpoint. See [Webhooks and Slack](/developers/webhooks). ### Issues (Error Tracking) Every error and crash your app reports shows up here — searchable, with the device/app context attached automatically. Open an issue to see its full stack trace, affected users, and an AI-generated summary to help you triage faster. ### Requests (Request Tracing) For backend projects, every request your service handled — its route, duration, status, the database queries and outbound calls it made — with per-route counts and latency percentiles, the busiest client addresses, and a waterfall for each kept request. Open an issue and the request that produced it is shown inline. The client-address policy (masked, hashed or full) lives under **Project settings → Tracing**. See [Request Tracing](/modules/request-tracing). ### Notifications Everything related to push notifications for this project: | Tab | What it's for | | - | - | | **Overview** | Total notifications sent, delivery rate, pending count, registered devices, and which platforms (iOS/Android) are configured | | **History** | Every notification ever sent, searchable and filterable by status | | **Send notification** | Compose and send to specific devices, your own user IDs, or a topic | | **Topics** | Create, view subscriber counts for, and delete topics | | **Devices** | Registered device tokens for this project | | **Apps** | The platform apps (iOS/Android) registered under this project | | **Settings** | Upload your Apple `.p8` key and Android `google-services.json` (a platform shows a **Configured** badge once it has credentials), and set up [identity verification](/modules/users-identity#identity-verification) | See [Push Notifications](/modules/push-notifications) for the full picture. ### Feedbacks What your users report from inside your apps, and how they answer your NPS survey: | Tab | What it's for | | - | - | | **Inbox** | Every feedback report, with screenshots, device details, breadcrumbs and the linked error. Filter, assign, change status, add internal notes, reply by email or push, or create a GitHub issue. | | **Surveys** | Your NPS survey: its wording, cadence, sampling and targeting, the score and its trend, and every response. | | **Settings** | The feedback form your apps show: email and screenshot modes, and labels. Changes reach your apps without a release. | See [Feedback](/modules/feedback) and [NPS surveys](/modules/nps-surveys). ### Analytics and Events **Analytics** shows active users (DAU, WAU, MAU), installs, sessions, crash-free rates per version, breakdowns you can click to filter, retention cohorts and funnels. **Events** lists every event your apps send, with a breakdown by property. See [Analytics](/modules/analytics). ### Release control Two tabs for what your users see when they open the app: | Tab | What it's for | | - | - | | **Updates** | One card per platform: the latest and minimum versions, blocked versions, optional, recommended or forced updates, a deadline, store links, the prompt's message in English and Arabic, and **Versions in the wild**. | | **Emergency** | **Maintenance mode** (now or scheduled, with an allow-list and a test mode), **Lift all**, **Propagation** (how many devices have picked up your last change), and **History**. | Changing anything here needs the **Manage app updates** permission (Owner and Admin). See [Release control](/modules/release-control/overview). ## Account settings Your personal profile, email, and account security live under your account menu, separate from any specific organization or project. > [!NOTE] > > The dashboard is where you configure and monitor your app. Everything your > app does at runtime — reporting errors, registering devices, sending > notifications — goes through the SDK or REST API, covered in the rest of > these docs. # Error & success codes Source: https://docs.lerix.dev/developers/errors-responses Response shapes and error codes for the Lerix REST API. ## Success response A successful request returns HTTP `200` or `201` with a JSON body: ```json theme={"dark"} { "success": true, "message": "Notification sent successfully" } ``` ## Error response shape Most errors return: ```json theme={"dark"} { "message": "This Project Not Founded", "error": "PROJECT_NOT_FOUND", "statusCode": 400 } ``` `error` is a stable, machine-readable code you can safely match on. `message` is a human-readable description and may change wording over time — don't parse it. > [!NOTE] > > A few less common failure paths (like exceeding your plan's notification > limit, or a resolved device token/key not being found) throw a generic Nest > exception instead of a coded one. For those, `error` falls back to the > generic HTTP reason phrase (e.g. `"Bad Request"`, `"Not Found"`) rather than > a specific code — check the HTTP status and `message` text in that case. ## Common error codes | HTTP status | `error` | Meaning | | - | - | - | | 400 | `PROJECT_NOT_FOUND` | `projectSlug` doesn't match any project | | 400 | `DONT_HAVE_ACCSESS_PROJECT` | The authenticated identity doesn't have access to this project | | 404 | `API_KEY_NOT_VALID` | `apiKey` doesn't match any key on this project | | 400 | `PROJECT_API_KEY_DONT_HAVE_PERMISSION` | The API key exists but isn't granted the permission this endpoint needs (e.g. Send Push Notifications) | | 400 | `NOTIFICATION_TARGET_INVALID` | A send combined `externalUserIds` with `deviceTokens` or `sendByTopic`. Use one target per request | | 404 | `TOPIC_NOT_FOUND` | `sendByTopic` was `true` but no topic matches `topic` for this project | | 400 | *(generic — "Bad Request")* | Notification limit reached for your plan | | 404 | *(generic — "Not Found")* | None of the given `deviceTokens` matched a registered device | | 400 | *(generic — "Bad Request")* | A device resolved to iOS, but no Apple push key is configured for this project yet — upload one from **Notifications → Settings** | | 404 | `NOTIFICATION_NOT_FOUND` | `notificationId` (the `{id}` path parameter) doesn't match any notification for this API key's project | | 404 | `SCHEDULED_NOTIFICATION_NOT_FOUND` | Same as `NOTIFICATION_NOT_FOUND`, returned by [Cancel a scheduled notification](/api-reference/cancel-notification) | | 400 | `CANNOT_CANCEL_SENT_NOTIFICATION` | [Cancel](/api-reference/cancel-notification) was called on a notification that isn't `scheduled` anymore | | 400 | `CANNOT_UPDATE_CANCELED_NOTIFICATION` | [Update](/api-reference/update-notification) was called on a `canceled` notification | | 400 | `CANNOT_UNSEND_NOTIFICATION` | [Unsend](/api-reference/unsend-notification) was called on a notification whose status isn't `sent` | ## Validation errors Missing or malformed required fields (e.g. an empty `title`) return `400` with a `message` array describing each failing field, in the standard NestJS validation-pipe shape: ```json theme={"dark"} { "message": ["title should not be empty"], "error": "Bad Request", "statusCode": 400 } ``` # Connect an AI agent (MCP) Source: https://docs.lerix.dev/developers/mcp Let Claude Code, Replit, or any other MCP-capable AI coding tool act on your Lerix account — list orgs and projects, triage issues, and connect a new project — from inside your normal chat. Lerix exposes a remote [MCP](https://modelcontextprotocol.io) server so an AI coding agent can work with your real Lerix data instead of you copying IDs and SDK keys back and forth by hand. The agent authenticates as *you*, through a normal browser login — it never sees or stores a password or API key, and it can only see and change what your own account already has permission to. > [!NOTE] > > Every write action (creating a project, resolving an issue, and so on) > requires the agent to pass an explicit confirmation flag, which a > well-behaved agent will only do after asking you. Read actions (listing > orgs, projects, issues) don't require confirmation. ## Connect Claude Code ```bash theme={"dark"} claude mcp add --transport http lerix https://mcp.lerix.dev/mcp ``` Then, in a Claude Code session, run: ``` /mcp ``` Pick **lerix** — your browser opens to the Lerix consent screen, where you log in (or reuse an existing session) and approve the connection. Claude Code stores and refreshes the resulting token itself; you only do this once per machine. If you'd rather install it as a full plugin (bundles a skill that sequences the tools for common workflows, like connecting a project end-to-end): ``` /plugin marketplace add lerix-dev/lerix-mcp-plugin /plugin install lerix@lerix ``` ## Connect Replit In Replit Agent: **Integrations → MCP Servers → + Add MCP server**, then paste: ``` https://mcp.lerix.dev/mcp ``` Replit discovers the OAuth configuration automatically and walks you through the same browser sign-in. ## Other MCP clients Any client that supports a remote MCP server over HTTP with OAuth 2.1 (dynamic client registration + PKCE) can connect the same way — point it at `https://mcp.lerix.dev/mcp` and complete the sign-in when prompted. There's no API key to generate or paste in. ## What it can do **Read** (no confirmation needed): | Tool | What it does | | - | - | | `list_organizations` | Organizations you belong to | | `list_projects` | Projects in an organization, with the platforms each one ships on | | `list_issues` | Deduplicated, filterable issue list for a project | | `get_issue_details` | Stack trace, occurrences, affected devices for one issue | | `search_events` | Raw occurrence counts grouped by type/severity/day | | `get_usage_stats` | Notification delivery and device/geo usage for a project | | `list_topics` | Push-notification topics configured for a project | | `get_push_config_status` | Which push providers (iOS/Android/Web) are configured for a project — never returns a secret | | `list_feedback` | The project's [feedback](/modules/feedback) inbox, filterable by `status`, `label` and a text `search`, paginated with `page` and `limit` (default 20, up to 100). Returns a message preview, per-status counts and a dashboard link per item; never the user's email address | | `get_feedback` | One feedback report (`feedbackId`): message, labels, device and app details, breadcrumbs and the linked error. Screenshots are summarized (count, type, size) with a dashboard link to view them | | `get_nps` | [NPS](/modules/nps-surveys) score, response rate, distribution and trend for the active survey (or `surveyId`), plus the latest detractor comments. Filters: `from`, `to` (YYYY-MM-DD, default the last 90 days), `platform`, `appVersion`, `environment` | | `analytics_overview` | [Analytics](/modules/analytics) overview: DAU/WAU/MAU, installs, sessions, crash-free rates and breakdowns. Filters: `from`, `to` (default the last 30 days), `environment` (default `prod`), `platform`, `appVersion` | | `top_events` | The most frequent events, with counts and unique users. Same filters as `analytics_overview`, plus `limit` (default 25, up to 200) and `includeTrend` to add daily counts | **Create / connect** (requires confirmation): | Tool | What it does | | - | - | | `create_organization` | Create a brand-new organization, owned by you | | `link_project` | Find or create a project, and return a real SDK init snippet for each platform. Pass every framework your product ships on in `platforms` (for example `["flutter", "nextjs"]`) so all your apps share one project; calling it again with a new platform adds it to the existing project | | `create_topic` | Create a push-notification topic | | `provision_web_push` | Generate VAPID keys for web push — no credential needed from you. Regenerates (and invalidates existing subscribers) if already configured | | `configure_android_push` | Store a Firebase service-account JSON you paste in, for Android/FCM | | `configure_ios_push` | Store an APNs `.p8` key's contents you paste in, plus Team ID and Key ID, for iOS | **Write** (requires confirmation): | Tool | What it does | | - | - | | `resolve_issue` | Mark an issue resolved | | `assign_issue` | Assign an issue to a team member | | `update_feedback_status` | Move one feedback report (`feedbackId`) to `new`, `in_progress`, `resolved`, `closed` or `spam`. Needs `confirm: true` and the same permission as resolving issues; every change is recorded in the audit log | ## Security notes * All access is scoped by your existing organization/project permissions — the agent can't see or change anything your account couldn't already. * Nothing the agent connects to can read your password. Sign-in happens on `app.lerix.dev` itself, the same login you already use. * To disconnect a client, remove the server from it directly (e.g. `claude mcp remove lerix`). If you believe a specific token was compromised, contact [support@lerix.dev](mailto:support@lerix.dev). * `configure_android_push` and `configure_ios_push` ask you to paste a real push-provider credential (a Firebase service-account key, or an APNs `.p8` key) into the chat. That text passes through your AI tool the same way any other message does — only paste it into a client and session you trust. # Webhooks and Slack Source: https://docs.lerix.dev/developers/webhooks Post new feedback, NPS detractors and new issues to Slack or to your own endpoint, signed so you can verify every request. ## Overview A webhook sends an HTTP `POST` to a URL you choose whenever something happens in a project. Use it to post to a Slack channel, open a ticket in your own system, or start an automation in Zapier. | Event | Sent when | | - | - | | `feedback.created` | A user sends [feedback](/modules/feedback) | | `nps.detractor` | A user answers the [NPS survey](/modules/nps-surveys) with a score from 0 to 6 | | `issue.created` | A new [issue](/modules/error-tracking) is reported for the first time | | `release_control.changed` | An update rule, a maintenance window or **Lift all** changes [release control](/modules/release-control/overview) | | `webhook.test` | You click **Test** in the dashboard | `issue.created` fires once per issue, not for every occurrence, so a crash loop doesn't flood your channel. ## Add a webhook **1. Open the settings** In the project, open **Settings → Webhooks & Slack** and click **Add webhook**. You need permission to change project settings. **2. Pick a destination** * **Webhook**: any `https://` URL that is publicly reachable. Lerix signs every request (see below). * **Slack**: an incoming-webhook URL, created in Slack under **Apps → Incoming Webhooks**. It starts with `https://hooks.slack.com/`. Lerix posts a formatted message with a link back to the dashboard. **3. Pick the events** Choose at least one of `feedback.created`, `nps.detractor`, `issue.created` and `release_control.changed`. **4. Copy the signing secret** For a generic webhook, the signing secret (`whsec_...`) is shown **once**, right after you create it. Store it with your endpoint's configuration. If you lose it, delete the webhook and add it again to get a new one. **5. Send a test** Click **Test** to send a `webhook.test` event. The dashboard shows the status code your endpoint answered, or the error if it couldn't be reached. You can pause a webhook without deleting it, and edit its URL or events at any time. ## Payload Every generic webhook request has a JSON body with the same envelope: ```json theme={"dark"} { "type": "feedback.created", "projectSlug": "my-shop", "createdAt": "2026-10-13T08:00:00.000Z", "data": { } } ``` And these headers: | Header | Value | | - | - | | `Content-Type` | `application/json` | | `Lerix-Event` | The event type, the same as `type` in the body | | `Lerix-Delivery` | A UUID for this delivery. Retries of the same delivery keep it, so use it to ignore duplicates. | | `Lerix-Signature` | `t=,v1=` | ### `data` by event **feedback.created** ```json theme={"dark"} { "id": "6f1c2a3e-...", "message": "Checkout button does nothing", "labels": ["bug"], "email": "user@example.com", "status": "new", "platform": "ios", "appVersion": "1.4.0", "environment": "prod", "externalId": "user-42", "attachmentCount": 1, "bugId": "b7d0...", "createdAt": "2026-10-13T08:00:00.000Z", "url": "https://app.lerix.dev/dashboard/project/my-shop/feedbacks/6f1c2a3e-..." } ``` `email`, `externalId` and `bugId` are `null` when they don't apply. `bugId` is set when the same install reported an error just before the feedback. **nps.detractor** ```json theme={"dark"} { "surveyId": "2a9e...", "responseId": "c41b...", "score": 4, "message": "Too many steps to pay", "platform": "android", "appVersion": "1.4.0", "environment": "prod", "externalId": "user-42", "answeredAt": "2026-10-13T08:00:00.000Z", "url": "https://app.lerix.dev/dashboard/project/my-shop/feedbacks/surveys/2a9e..." } ``` `message` is `null` if the user didn't answer the follow-up question. **issue.created** ```json theme={"dark"} { "id": "b7d0...", "title": "TypeError: Cannot read properties of undefined", "severity": "critical", "type": "crash", "fingerprint": "3f9a...", "url": "https://app.lerix.dev/dashboard/project/my-shop/issues/b7d0..." } ``` **release\_control.changed** ```json theme={"dark"} { "action": "maintenance.create", "targetId": "0b6f3c1e-...", "actor": { "id": "5d2a...", "name": "Ana", "kind": "user" }, "reason": "Database migration", "url": "https://app.lerix.dev/dashboard/project/my-shop/release-control" } ``` `action` is one of `rule.upsert`, `rule.delete`, `maintenance.create`, `maintenance.update`, `maintenance.end`, `lift_all` and `app.store_update`. See [Events, webhooks and permissions](/modules/release-control/events-webhooks#webhook). **webhook.test** ```json theme={"dark"} { "message": "This is a test delivery from Lerix.", "triggeredBy": "Ana" } ``` New fields may be added to `data` over time. Ignore fields you don't know rather than rejecting the request. ## Verify the signature `Lerix-Signature` is an HMAC-SHA256 of the timestamp and the raw body, `t + "." + body`, keyed with the webhook's signing secret. Check it before you trust a request: 1. Split the header on `,` and read `t` and `v1`. 2. Reject the request if `t` is more than 5 minutes away from your clock. This stops an old delivery from being replayed. 3. Compute `HMAC-SHA256(secret, t + "." + rawBody)` as hex and compare it to `v1` in constant time. Use the **raw** request body, exactly as received. Parsing the JSON and serializing it again changes the bytes and breaks the signature. ```ts Node.js (Express) theme={"dark"} import crypto from 'node:crypto'; import express from 'express'; const secret = process.env.LERIX_WEBHOOK_SECRET!; // whsec_... function verifyLerixSignature(rawBody: string, header: string, toleranceSeconds = 300): boolean { const parts = Object.fromEntries( header.split(',').map((part) => { const i = part.indexOf('='); return [part.slice(0, i).trim(), part.slice(i + 1).trim()]; }), ); const t = Number(parts.t); if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const a = Buffer.from(parts.v1 ?? '', 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b); } const app = express(); app.post('/lerix/webhook', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); if (!verifyLerixSignature(rawBody, req.header('Lerix-Signature') ?? '')) { return res.status(400).send('invalid signature'); } const event = JSON.parse(rawBody); // Use req.header('Lerix-Delivery') to skip deliveries you've already handled. console.log(event.type, event.data); res.sendStatus(200); }); ``` ```python Python (Flask) theme={"dark"} import hashlib import hmac import json import os import time from flask import Flask, abort, request SECRET = os.environ["LERIX_WEBHOOK_SECRET"] # whsec_... def verify_lerix_signature(raw_body: bytes, header: str, tolerance_seconds: int = 300) -> bool: parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part) try: t = int(parts.get("t", "")) except ValueError: return False if abs(time.time() - t) > tolerance_seconds: return False signed = f"{t}.".encode() + raw_body expected = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) app = Flask(__name__) @app.post("/lerix/webhook") def lerix_webhook(): raw_body = request.get_data() # the raw bytes, before any JSON parsing if not verify_lerix_signature(raw_body, request.headers.get("Lerix-Signature", "")): abort(400) event = json.loads(raw_body) # Use request.headers["Lerix-Delivery"] to skip deliveries you've already handled. print(event["type"], event["data"]) return "", 200 ``` Slack deliveries aren't signed: Slack's incoming-webhook URL is itself the secret, so keep it private. ## Delivery and retries * Lerix waits up to **5 seconds** for your endpoint. Answer with any `2xx` status quickly, and do slow work after responding. * On a network error, a timeout, `429` or a `5xx`, Lerix retries up to **3 times**, after 2, 10 and 30 seconds. Every attempt is signed again with a fresh timestamp and keeps the same `Lerix-Delivery` ID. * Any other status (for example `400` or `404`) is not retried. * Redirects aren't followed. * The URL must use `https` and resolve to a public address. Private, loopback and link-local addresses are refused, both when you save the webhook and when each request is sent. Deliveries are not guaranteed to arrive in order. Use `createdAt` in the body if order matters to you. # Android (Kotlin) — Error Tracking Source: https://docs.lerix.dev/frameworks/android/error-tracking Report errors and automatically capture crashes from your native Android app to the Lerix dashboard. ## Introduction Lerix captures errors and crashes from your native Android app so you can debug issues faster. After you complete the [Android installation](/frameworks/android/installation), use `Lerix.throwError` to send custom errors or caught exceptions to your dashboard — uncaught crashes are captured automatically, with no extra code required. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.initialize()` before reporting errors. ## Report an error Use `Lerix.throwError` to manually report an error. This is useful when you catch an exception yourself and want to send it to Lerix with extra context. ```kotlin theme={"dark"} import com.lerix.sdk.Lerix import com.lerix.sdk.models.BugSeverity import com.lerix.sdk.models.BugType try { riskyOperation() } catch (e: Exception) { Lerix.throwError( issue = e.toString(), stack = e.stackTrace.map { it.toString() }, type = BugType.RUNTIME_ERROR, severity = BugSeverity.HIGH, metadata = mapOf("userId" to "123", "action" to "payment", "amount" to 100), ) } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `issue` | `String` | Yes | Error message or description | | `stack` | `List` | No | Stack trace frames — pass `e.stackTrace.map { it.toString() }` for a caught exception's frames | | `type` | `BugType` | No | Bug classification (default: `RUNTIME_ERROR`) | | `severity` | `BugSeverity` | No | Severity level (default: `MEDIUM`) | | `metadata` | `Map?` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception thrown at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't throw | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness, jank, or excessive resource use | | `COMPATIBILITY` | Breaks on a specific device, OS version, or platform | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The app terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## Automatic crash capture `Lerix.initialize()` installs a crash handler by default, wrapping Java/Kotlin's global `Thread.setDefaultUncaughtExceptionHandler` — any uncaught exception on any thread is captured, regardless of which thread it occurs on. A crash can't reliably make a network call — the process is already terminating — so the report is written to disk synchronously at crash time, then sent automatically the *next* time your app launches, tagged `type: CRASH, severity: CRITICAL`. You don't need to do anything to receive it; it shows up in the dashboard on the following launch. To disable it (e.g. if your app already has its own crash reporter and you only want manual `throwError` calls): ```kotlin theme={"dark"} Lerix.initialize( context = this, apiKey = "your-api-key", projectId = "your-project-id", enableCrashReporting = false, ) ``` ### Verify the integration Throw a test error to confirm everything is working: ```kotlin theme={"dark"} Lerix.throwError("Test error") ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. ## Filtering errors Requires the Android SDK 1.2.0 or later. Drop noisy errors or scrub data before it leaves the device. Filtering applies to every error the SDK sends: `Lerix.throwError` and uncaught crashes (checked when the crash is sent on the next launch). ```kotlin theme={"dark"} Lerix.initialize( context = this, apiKey = "YOUR_PROJECT_API_KEY", projectId = "YOUR_PROJECT_ID", // Dropped when the message contains any of these (case-insensitive). ignoreErrors = listOf("Read timed out", "Software caused connection abort"), // Dropped when the message matches any of these regexes (containsMatchIn). ignoreErrorPatterns = listOf(Regex("^java\\.net\\.\\w+Exception")), // Runs after the ignore lists. Return the event (optionally modified) to // send it, or null to drop it. beforeSend = { event -> if (event.metadata?.get("debugOnly") == true) null else event.copy(message = event.message.replace(Regex("token=\\S+"), "token=[redacted]")) }, ) ``` * `ignoreErrors` (`List`) and `ignoreErrorPatterns` (`List`) run first. Empty or whitespace-only `ignoreErrors` entries are skipped. * `beforeSend` then gets a `LerixErrorEvent` with `message`, `stack`, `type`, `severity` and `metadata`. Return it (changed or not) to send it, or `null` to drop it. It is called on a background thread. * If `beforeSend` throws, the original event is sent and the exception is swallowed (logged in `debugMode`). * The message you match against is the issue title, which includes the error class. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # Android (Kotlin) — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/android/feedback-analytics Add in-app feedback, shake to report, an NPS survey and product analytics to your native Android app. Requires `lerix` 1.3.0 or later. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). ## Setup Call `Lerix.initialize()` from `Application.onCreate()`, as in [Installation](/frameworks/android/installation). Analytics starts on its own: Activities are tracked through the application's lifecycle callbacks, so there is nothing to add to each screen. Two optional parameters configure these modules: ```kotlin MyApplication.kt theme={"dark"} Lerix.initialize( context = this, apiKey = "your-api-key", projectId = "your-project-id", environment = "staging", // default: dev for debuggable builds and emulators, else prod analytics = LerixAnalyticsOptions( enabled = true, autoSessions = true, autoScreens = true, // a $screen_view per resumed Activity trackFragments = false, // true: also one per resumed AndroidX Fragment ), ) ``` | Parameter | Type | Description | | - | - | - | | `environment` | `String?` | Sent with feedback, survey responses and events | | `analytics` | `LerixAnalyticsOptions` | `enabled`, `autoSessions`, `autoScreens` (default `true`) and `trackFragments` (default `false`) | The SDK declares its own feedback Activity through manifest merging, so there's nothing to add to your `AndroidManifest.xml`. ## In-app feedback ```kotlin theme={"dark"} Lerix.feedback.show(activity) { result -> if (result.submitted) Log.d("App", "feedback ${result.clientId}") } ``` The current screen is captured before the UI appears, with `PixelCopy` on Android 8+ so maps and video are included. The flow is message, labels, screenshots (tap one to draw on it with 4 colours, 3 pen sizes, undo and redo; **Add screenshot** captures another screen), email, then a review with **Show details**. Screens that set `FLAG_SECURE` are never captured. `result` is a `LerixFeedbackResult(submitted, id, clientId)`. `id` is `null` while the feedback is still queued. ### Options Labels, the email mode and the screenshot mode come from the dashboard. Override them for one call: ```kotlin theme={"dark"} Lerix.feedback.show( activity, LerixFeedbackOptions( labels = listOf("bug", "improvement"), email = LerixFieldMode.REQUIRED, // HIDDEN, OPTIONAL, REQUIRED screenshot = LerixFieldMode.OPTIONAL, // HIDDEN or OPTIONAL prefillEmail = user.email, custom = mapOf("plan" to "pro"), ), ) Lerix.feedback.setUserEmail(user.email) // prefill for every flow ``` ### Shake to report ```kotlin theme={"dark"} Lerix.feedback.enableShakeToReport(application) // Later, to stop: Lerix.feedback.disableShakeToReport() ``` ### Hide sensitive views ```kotlin theme={"dark"} Lerix.markConfidential(cardNumberView) // a black box in screenshots Lerix.unmarkConfidential(cardNumberView) ``` ### Your own UI ```kotlin theme={"dark"} lifecycleScope.launch { val result = Lerix.feedback.submit( message = "Checkout button does nothing", labels = listOf("bug"), email = "user@example.com", screenshots = listOf(pngBytes), // PNG, JPEG or WebP, up to 5 MB each ) } ``` ### Language, dark mode and colours The UI ships in English and Arabic and follows the system light or dark mode. Layouts mirror in Arabic when your app declares `android:supportsRtl="true"`. The accent colour is your theme's `colorPrimary`; override it: ```kotlin theme={"dark"} Lerix.setAccentColor(0xFF6E56CF.toInt()) ``` Override any `lerix_*` string by defining a string resource with the same name in your app. ## NPS survey ```kotlin theme={"dark"} lifecycleScope.launch { val shown = Lerix.surveys.maybeShowNps(activity) } // Without coroutines Lerix.surveys.maybeShowNps(activity) { shown -> } // While testing: skip the eligibility rules lifecycleScope.launch { Lerix.surveys.maybeShowNps(activity, force = true) } ``` The SDK shows the survey only when this install is due (see [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown)). In `debugMode`, Logcat says why a survey was skipped and when it is next due. ## Analytics ```kotlin theme={"dark"} Lerix.track("checkout_started", mapOf("items" to 3, "coupon" to "EID")) Lerix.screen("Checkout") // manual screen view (Compose, custom navigation) Lerix.analytics.flush() // send now Lerix.analytics.setEnabled(false) // opt out: stops recording, clears the queue, persisted ``` * `$first_open`, `$session_start` / `$session_end`, `$app_update` and a `$screen_view` per resumed Activity (its class name) are recorded automatically. * With Jetpack Compose or a single-Activity app, call `Lerix.screen()` when your destination changes. * A process started in the background (an FCM data message, a service) isn't counted as an app start or a session. * `track` never throws. Invalid events and properties are dropped and logged in `debugMode`. > [!NOTE] > > Call [`Lerix.setUser`](/modules/users-identity) after sign-in. Analytics > then counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs The last 50 breadcrumbs are attached to feedback. Screen changes are added automatically. ```kotlin theme={"dark"} Lerix.addBreadcrumb("Added item to cart", category = "ui", data = mapOf("sku" to "A1")) ``` Categories: `navigation`, `log`, `http`, `ui`, `custom` (the default). # Android (Kotlin) — Installation Source: https://docs.lerix.dev/frameworks/android/installation Add Lerix to your native Android app in a few steps — no Flutter required. ## Requirements * Android 7.0+ (API 24) * Kotlin 1.9+, Android Gradle Plugin 8.5+ > [!NOTE] > > This is the native Kotlin library for apps with no Flutter involved. If your app is built with Flutter, use the [Flutter SDK](/frameworks/flutter/installation) instead — it already covers iOS, Android, and Web from one package. ## 1. Add the dependency The SDK is distributed via [JitPack](https://jitpack.io/#lerixDev/lerix_android), built directly from GitHub releases. Add the JitPack repository in `settings.gradle.kts`: ```kotlin settings.gradle.kts theme={"dark"} dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } ``` Then add the dependency in your app's `build.gradle.kts`: ```kotlin build.gradle.kts theme={"dark"} dependencies { implementation("com.github.lerixDev.lerix_android:lerix:1.5.0") } ``` Replace `1.5.0` with the version you want to pin to — see the [releases page](https://github.com/lerixDev/lerix_android/releases) for available versions. ## 2. Initialize the SDK Call `Lerix.initialize()` as early as possible — from a custom `Application.onCreate()`. This registers your app/device with the backend and installs automatic crash reporting. ```kotlin LerixExampleApp.kt theme={"dark"} import android.app.Application import com.lerix.sdk.Lerix class MyApplication : Application() { override fun onCreate() { super.onCreate() Lerix.initialize( context = this, apiKey = "your-api-key", projectId = "your-project-id", debugMode = true, ) } } ``` Register the class in your `AndroidManifest.xml`: ```xml AndroidManifest.xml theme={"dark"} ``` > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `context` | `Context` | Yes | Any application/activity context | | `apiKey` | `String` | Yes | Your project API key from the dashboard | | `projectId` | `String` | Yes | Your project ID (slug) from the dashboard | | `url` | `String` | No | Backend base URL. Defaults to `https://api.lerix.dev/v1` — only change this if you're pointing at a self-hosted backend | | `debugMode` | `Boolean` | No | Print SDK request/response logs to Logcat (default: `false`) | | `enableCrashReporting` | `Boolean` | No | Install the automatic crash handler (default: `true`) — see [Error Tracking](/frameworks/android/error-tracking) | | `onError` | `((Throwable) -> Unit)?` | No | Called if initialization itself fails (e.g. network unreachable) | ## 3. Verify the integration Throw a test error to confirm everything is working: ```kotlin theme={"dark"} Lerix.throwError("Test error") ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/android/error-tracking) to report custom errors and enable automatic crash capture, or [Push Notifications](/frameworks/android/push-notifications) to enable push delivery. # Android (Kotlin) — Push Notifications Source: https://docs.lerix.dev/frameworks/android/push-notifications Configure Firebase Cloud Messaging (FCM) delivery for your native Android app — with zero Firebase setup on your part. ## Introduction Lerix delivers push notifications on Android using Firebase Cloud Messaging (FCM) — but unlike a typical FCM integration, **you don't add a `google-services.json` file or the Google Services Gradle plugin to your app**. The SDK registers your device against Lerix's own Firebase project internally, using this Lerix project's Sender ID (fetched from the backend automatically). After you complete the [Android installation](/frameworks/android/installation), follow the steps below to enable push delivery. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.initialize()` before configuring push notifications. ## Dashboard configuration For the backend to actually send to your devices, it needs to authenticate as *your own* Firebase project. This is the only Firebase-related step required: 1. Create or open your app in the [Firebase Console](https://console.firebase.google.com/). 2. Go to **Project Settings → Service Accounts → Generate new private key**. 3. Upload the downloaded JSON file to the Lerix dashboard, under **Notifications → Settings**. Once uploaded, the **Android** card in **Notifications → Settings** shows a **Configured** badge — click it again any time to review it or replace it. ## Manifest permissions and components Nothing to add here — the SDK's own manifest already declares the `INTERNET` and `POST_NOTIFICATIONS` permissions, its Firebase Messaging service, and its notification-tap receiver. Android's manifest merger folds all of it into your app automatically; there's no service, receiver, or permission for you to declare yourself. ## Request permission and register ```kotlin theme={"dark"} import com.lerix.sdk.Lerix import com.lerix.sdk.notifications.LerixPermissionStatus lifecycleScope.launch { val granted = Lerix.notifications.requestPermissions() } Lerix.notifications.setOnNotificationReceived { payload -> // Fired while the app is open (foreground) — show an in-app banner, etc. Log.d("MyApp", "Notification received: ${payload.title} — ${payload.body}") } Lerix.notifications.setOnNotificationTapped { payload -> // Fired when the user taps the notification — including one that // launched the app from a fully closed state (see the note below). Log.d("MyApp", "Notification tapped: ${payload.metadata}") } val status = Lerix.notifications.checkPermissionStatus() // LerixPermissionStatus.AUTHORIZED, .DENIED, .NOT_DETERMINED ``` `requestPermissions()` is a suspend function — call it from a coroutine scope (e.g. `lifecycleScope.launch`). On Android 13+ it requests the runtime `POST_NOTIFICATIONS` permission and, once granted, fetches and registers the FCM token; on older Android versions there's no runtime permission to request, so it registers immediately. Register `setOnNotificationTapped` somewhere with access to your navigation, since a tap almost always means routing to a specific screen based on `payload.metadata` rather than just logging it. > [!NOTE] > > A tap is delivered reliably even when it's what launched the app from a > fully terminated state — call `setOnNotificationTapped` as soon as your > launcher Activity starts, and any tap that happened before your app > finished starting up is replayed automatically once the callback is > registered. Use `Lerix.notifications.getInitialNotificationTap()` if you > need to check for one synchronously instead. ## The notification payload `setOnNotificationReceived` and `setOnNotificationTapped` both receive a `LerixNotificationPayload`: ```kotlin theme={"dark"} data class LerixNotificationPayload( val notificationId: String?, // this notification's id in Lerix val title: String?, val body: String?, val imageUrl: String?, // whatever you passed as `imageUrl` when sending, if any val metadata: Map, // whatever you passed as `metadata` when sending ) ``` ```kotlin theme={"dark"} Lerix.notifications.setOnNotificationTapped { payload -> val screen = payload.metadata["screen"] val orderId = payload.metadata["orderId"] // Navigate based on payload.metadata, etc. } ``` ## Device identifiers Three different methods return three different things — use the right one for what you're doing: | Method | Returns | Use it for | | - | - | - | | `getRegisteredTokenId()` | Lerix's own id for this registered device, assigned once `requestPermissions()` succeeds and the token is registered with the backend | The value to put in **device tokens** when sending a notification to this specific device — from the dashboard's **Send notification** tab, or the [REST API](/api-reference/push-notifications) | | `getDeviceToken()` | The raw FCM registration token | Rarely needed directly; useful for diagnostics or if you're calling FCM yourself outside of Lerix | | `getDeviceId()` | `Settings.Secure.ANDROID_ID` — a local Android system identifier, unrelated to Lerix's backend | Local diagnostics only; **not** what the dashboard's send flow expects | ```kotlin theme={"dark"} val tokenId = Lerix.notifications.getRegisteredTokenId() Log.d("MyApp", "Send test notifications to: $tokenId") ``` > [!WARNING] > > `getRegisteredTokenId()` is the Android equivalent of the Flutter SDK's > `getDeviceId()` — the naming differs between the two SDKs, but both return > the same kind of value: the id the dashboard and REST API expect in their > device-targeting field. Pasting this SDK's `getDeviceId()` (the > `ANDROID_ID` value) into the dashboard's send flow will fail with a "not > found" error — it was never registered with the backend. All three return `null` until notification permission is granted and registration completes. ## Sending a custom sound Set `sound` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`sound` parameter](/api-reference/push-notifications) — to play a bundled sound file instead of the device default. * The file must already ship inside your app under `res/raw/` — Lerix only passes the filename through, it never hosts or uploads sound files. * Pass the filename **without** its extension, e.g. `notif` for `res/raw/notif.mp3`. Supported formats: `.wav`, `.mp3`, `.ogg`. > [!NOTE] > > A sound is locked in the first time it's used on a given device — Android > fixes a notification channel's sound at creation and never lets it change > afterward. Sending a different `sound` value later creates a new channel > rather than updating the old one; that's expected Android behavior, not a > bug. No SDK code is required to play the sound — it happens automatically as part of the system notification. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the device inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```kotlin theme={"dark"} Lerix.notifications.setOnNotificationTapped { payload -> val chatId = payload.metadata["chatId"] // Navigate to the chat screen, etc. } ``` ## Sending an image Set `imageUrl` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications) — to show a rich image in the notification. This works automatically on Android, with no extra setup: the SDK's bundled messaging service downloads the image and renders it as a `BigPictureStyle` notification. The image is also available in Kotlin as `payload.imageUrl` (in `setOnNotificationReceived`/`setOnNotificationTapped`) if you want to show it in a custom in-app banner, in addition to the OS rendering it natively. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```kotlin theme={"dark"} Lerix.setUser("user_123") // after login Lerix.clearUser() // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```kotlin theme={"dark"} lifecycleScope.launch { Lerix.notifications.subscribeToTopic("promotions") Lerix.notifications.unsubscribeFromTopic("promotions") } ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. # Release control in Kotlin Source: https://docs.lerix.dev/frameworks/android/release-control Show update prompts, required updates and maintenance screens in your native Android app, with optional Google Play in-app updates. For what release control does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default once `Lerix.initialize()` runs, with the built-in screens. To change it, pass `appControl`: ```kotlin MyApplication.kt theme={"dark"} Lerix.initialize( context = this, apiKey = "your-api-key", projectId = "your-project-id", appControl = LerixAppControlOptions( enabled = true, // false: no checks, no screens, no events ui = true, // false: draw your own screens usePlayInAppUpdate = false, // true: Google Play's in-app update for required updates ), ) ``` | Option | Default | Description | | - | - | - | | `enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | | `usePlayInAppUpdate` | `false` | When the app was installed from Google Play and Play offers the update, a required update starts Play's full-screen in-app update instead of opening the store page. Other installs still open the store link. | ### Google Play in-app updates `usePlayInAppUpdate` needs Play's in-app update library in your app. The SDK doesn't bundle it, so add it yourself: ```kotlin app/build.gradle.kts theme={"dark"} dependencies { implementation("com.google.android.play:app-update:2.1.0") } ``` Without the library, or for apps not installed from Google Play, **Update** opens the store link as usual. ### Version The SDK sends `versionName` as the version and `versionCode` as the build number, and the package name so Lerix can build the Google Play link. Rules compare build numbers when both sides have one. ### Instant changes with silent push When you start or end a maintenance window or force an update, Lerix sends a silent FCM data message so apps in the background refresh too. It reaches apps with [push notifications](/frameworks/android/push-notifications) set up, and the SDK handles it for you: it is never shown as a notification. Without push, apps pick up the change at their next check. ## Built-in screens * **Maintenance**: a full-screen activity over your app, with a **Try again** button. **Back** sends the app to the background; it never returns into the app. If another of your activities comes to the front, the screen comes back on top. * **Required update**: the same full-screen host, with release notes and an **Update** button. * **Update prompt**: a dialog on the current activity with **Update**, **Later** and, when allowed, **Skip this version**. Back closes it without saving **Later**. The SDK declares its activity through manifest merging, so there's nothing to add to your `AndroidManifest.xml`. Buttons follow the device language (English, or Arabic right to left). See [Custom UI](/modules/release-control/custom-ui#built-in-screens) for when each one appears. ## Your own screens Set `ui = false`, then collect `Lerix.appControl.state`, a `StateFlow`. The state has `update` (`LerixAppUpdate`), `maintenance` (`LerixMaintenance`) and `sdk` (`LerixSdkControl`), each `null` when nothing applies (fields in [The state](/modules/release-control/custom-ui#the-state)). `Lerix.appControl.currentState` reads it once. ```kotlin MainActivity.kt theme={"dark"} lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { Lerix.appControl.state.collect { state -> val maintenance = state.maintenance val update = state.update when { maintenance != null -> showMaintenance(maintenance.title, maintenance.message) update != null && update.state == LerixUpdateState.REQUIRED -> showForceUpdate(update.message) else -> hideReleaseScreens() } } } } ``` Outside a coroutine, use a listener. It is called on the main thread: ```kotlin theme={"dark"} val listener = LerixAppControlListener { state -> render(state) } Lerix.appControl.addListener(listener) Lerix.appControl.removeListener(listener) ``` The helpers drive the buttons: ```kotlin theme={"dark"} Lerix.appControl.refresh { state -> render(state) } // Try again (or the suspend refresh()) Lerix.appControl.openStore(activity) // Update; returns whether something opened Lerix.appControl.later() // hide for remindAfterHours Lerix.appControl.skip() // hide until a newer version (optional updates only) ``` Pass the current activity to `openStore` so Play's in-app update can start from it. Without one, the SDK uses the activity in the foreground. ## Check now `Lerix.appControl.refresh()` is a `suspend` function that ignores the saved answer's lifetime, fetches, and returns the new state. `refresh(callback)` does the same from Java-style code. The SDK already checks on launch, when the app returns to the foreground, and every minute during an emergency (see [Delivery and offline behaviour](/modules/release-control/delivery-offline)). # Angular — Error Tracking Source: https://docs.lerix.dev/frameworks/angular/error-tracking Report errors and automatically capture uncaught exceptions from your Angular app to the Lerix dashboard. ## Introduction Lerix captures errors from your Angular app so you can debug issues faster. After you complete the [Angular installation](/frameworks/angular/installation), use `LerixService.throwError()` to send custom errors to your dashboard — uncaught errors are captured automatically, with no extra code required. > [!NOTE] > > Make sure you have registered the SDK with `provideLerix()` before reporting errors. ## Report an error ```ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixService, BugSeverity } from '@lerix-dev/lerix-angular'; @Component({ ... }) export class MyComponent { private readonly lerix = inject(LerixService); async onSomethingFailed() { try { await this.riskyOperation(); } catch (error) { this.lerix.throwError( String(error), undefined, undefined, BugSeverity.HIGH, { userId: '123', action: 'payment', amount: 100 }, ); } } } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `issue` | `string` | Yes | Error message or description | | `stack` | `string[]` | No | Stack trace frames. If omitted, one is captured automatically from the call site — see [Automatic stack capture](#automatic-stack-capture) below | | `type` | `BugType` | No | Bug classification (default: `RUNTIME_ERROR`) | | `severity` | `BugSeverity` | No | Severity level (default: `MEDIUM`) | | `metadata` | `Record` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception thrown at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't throw | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness, jank, or excessive resource use | | `COMPATIBILITY` | Breaks on a specific browser or device | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The app terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## Automatic error capture `provideLerix()` installs error capture two ways, so nothing needs wrapping in a `try`/`catch` for baseline coverage: * A custom Angular `ErrorHandler` catches anything thrown inside Angular's zone — template bindings, component lifecycle hooks, RxJS subscriptions. * Global `window.onerror` and `unhandledrejection` listeners catch everything else. Unlike a native mobile crash handler, a browser tab isn't about to terminate when one of these fires, so the report is sent immediately rather than persisted for the next launch. To disable both (e.g. if your app already has its own error reporting and you only want manual `throwError` calls): ```ts theme={"dark"} provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', enableCrashReporting: false, }) ``` ## Automatic stack capture When you call `throwError()` without a `stack` argument, one is captured automatically from the call site — you only need to pass one explicitly when reporting a caught `Error`'s own stack instead (e.g. `error.stack?.split('\n')`). Either way, every stack frame is resolved from its compiled/bundled position (e.g. `main.js:577:15`) back to the original source file and line, using the build's own source map — the same mechanism browser DevTools use to show you readable stack traces instead of minified bundle positions. This happens automatically; there's nothing to configure. > [!NOTE] > > Resolution requires a source map to be reachable at the frame's URL — > always true in development, and true in production only if your build > serves its `.map` files publicly. If your production build keeps source > maps private (a common choice), frames fall back to their compiled > position rather than failing to report — resolution is best-effort and > never blocks or delays the report itself. > [!TIP] > > To get readable production stack traces while keeping your source maps > private, upload them to Lerix after each build. See > [Angular source maps](/frameworks/angular/source-maps). ### Verify the integration ```ts theme={"dark"} this.lerix.throwError('Test error'); ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds, with a resolved stack trace pointing at this exact line. ## Filtering errors Requires `@lerix-dev/lerix-angular` 1.2.0 or later. Two optional `provideLerix()` options decide which errors are sent. They apply to every report: the Angular `ErrorHandler`, the global handlers and `LerixService.throwError`. ```ts app.config.ts theme={"dark"} provideLerix({ apiKey: 'YOUR_PROJECT_API_KEY', projectId: 'YOUR_PROJECT_ID', ignoreErrors: ['ResizeObserver loop', /^ChunkLoadError/], beforeSend: (event) => (event.message.includes('extension://') ? null : event), }), ``` * `ignoreErrors` runs first. A string drops any error whose message contains it, ignoring case. A `RegExp` drops any error whose message it matches. Empty strings are skipped. * `beforeSend` then sees only what's left. Return the event (changed or not) to send it, or `null` to drop it. If it throws, the original event is sent and the exception never reaches your app. * The message you match against is the issue title, which includes the error class, for example `TypeError: boom`. `beforeSend` receives a `LerixErrorEvent`: `{ message, stack, type, severity, metadata? }`. `stack` is an array of frames, already resolved through your source maps. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # Angular — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/angular/feedback-analytics Add an in-app feedback dialog, an NPS survey and product analytics to your Angular app, with Router screen tracking. Requires `@lerix-dev/lerix-angular` 1.3.0 or later. `@angular/router` is a peer dependency. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). ## Setup Add `provideLerixRouterTracking()` next to the `provideLerix()` from [Installation](/frameworks/angular/installation). It records a `$screen_view` on every Router `NavigationEnd`, and turns off the SDK's History API tracking so nothing is counted twice. ```ts app.config.ts theme={"dark"} import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideLerix, provideLerixRouterTracking } from '@lerix-dev/lerix-angular'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', environment: 'staging', // default: dev on localhost / *.local or in debug mode, else prod }), provideLerixRouterTracking({ name: (path) => path.replace(/^\/orders\/[^/]+/, '/orders/:id'), }), ], }; ``` The optional `name` maps a path (without query or hash) to a screen name, so `/orders/42` and `/orders/43` count as one screen. Return `null` to skip a path. `provideLerix()` also takes `locale`, `analytics` (`enabled`, `autoSessions`, `autoScreens`), `feedback` (`theme`, `primaryColor`, `strings`, `zIndex`) and `captureConsoleErrors`. They work as in [React](/frameworks/react/feedback-analytics#setup). ## In-app feedback ```ts toolbar.component.ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixConfidentialDirective, LerixFeedbackService } from '@lerix-dev/lerix-angular'; @Component({ selector: 'app-toolbar', imports: [LerixConfidentialDirective], template: `

{{ email }}

`, }) export class ToolbarComponent { readonly feedback = inject(LerixFeedbackService); email = 'user@example.com'; } ``` `LerixFeedbackService` has: | Method | Description | | - | - | | `show(options?)` | Opens the dialog. Options: `labels`, `email` (`hidden`, `optional`, `required`), `screenshot` (`hidden`, `optional`), `prefillEmail`, `custom`. Resolves `{ submitted, id?, clientId? }`. | | `submit(input)` | Sends feedback from your own form: `{ message, labels?, email?, screenshots?: Blob[], custom? }`. Resolves `{ clientId, id?, status }`. | | `setUserEmail(email)` | Prefills the email field. | | `getConfig()` | The project's feedback settings, as the dialog sees them. | The dialog renders inside a Shadow DOM, is a lazy chunk loaded on first use, and ships English and Arabic (right to left). The `lerixConfidential` directive adds `data-lerix-mask`, which blacks the element out of screenshots. ## NPS survey ```ts theme={"dark"} import { AfterViewInit, Component, inject } from '@angular/core'; import { LerixSurveyService } from '@lerix-dev/lerix-angular'; @Component({ selector: 'app-home', template: `...` }) export class HomeComponent implements AfterViewInit { private readonly surveys = inject(LerixSurveyService); ngAfterViewInit() { void this.surveys.maybeShowNps(); // only when this install is due } } ``` Pass `{ force: true }` while testing to skip the rules. See [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown). ## Analytics ```ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixAnalyticsService } from '@lerix-dev/lerix-angular'; @Component({ selector: 'app-checkout', template: `...` }) export class CheckoutComponent { private readonly analytics = inject(LerixAnalyticsService); checkout() { this.analytics.track('checkout_started', { items: 3 }); } } ``` `LerixAnalyticsService` has `track`, `screen`, `flush`, `setEnabled` (the persisted opt-out), `isEnabled`, `getSessionId` and `addBreadcrumb`. `$first_open`, `$session_start` / `$session_end`, `$app_update` and `$screen_view` are recorded automatically. `track` never throws. > [!NOTE] > > Call [`setUser`](/modules/users-identity) after sign-in. Analytics then > counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs ```ts theme={"dark"} this.analytics.addBreadcrumb('Applied coupon', 'ui', { code: 'EID' }); ``` The last 50 breadcrumbs (navigation is added automatically) are attached to feedback. Categories: `navigation`, `log`, `http`, `ui`, `custom`. # Angular — Installation Source: https://docs.lerix.dev/frameworks/angular/installation Add Lerix to your Angular app in a few steps. ## Requirements * Angular 19+ > [!NOTE] > > This is a dedicated Angular library — an injectable `LerixService`, a > `provideLerix()` function for your `app.config.ts`, and an Angular > `ErrorHandler` integration — built on top of `@lerix-dev/lerix-core`, the > framework-agnostic engine shared by every Lerix web SDK. Building a React > app instead? See the [React SDK](/frameworks/react/installation). If > you're using neither Angular nor React, `@lerix-dev/lerix-core` also works directly. ## 1. Add the dependency ```bash theme={"dark"} npm install @lerix-dev/lerix-angular ``` ## 2. Register the SDK Add `provideLerix()` to your `app.config.ts`: ```ts app.config.ts theme={"dark"} import { ApplicationConfig } from '@angular/core'; import { provideLerix } from '@lerix-dev/lerix-angular'; export const appConfig: ApplicationConfig = { providers: [ provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', debugMode: true, }), ], }; ``` > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. Registration with the backend, and the automatic crash handler, run at bootstrap — before your app becomes interactive. There's no separate `init()` call to make. ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `apiKey` | `string` | Yes | Your project API key from the dashboard | | `projectId` | `string` | Yes | Your project ID (slug) from the dashboard | | `url` | `string` | No | Backend base URL. Defaults to `https://api.lerix.dev/v1` — only change this if you're pointing at a self-hosted backend | | `release` | `string` | No | The version this build ships as (e.g. `1.4.0` or a commit SHA). Pass the same value when uploading source maps so production stack traces show your original files: see [Source maps](/frameworks/angular/source-maps) | | `debugMode` | `boolean` | No | Print SDK request/response logs to the console (default: `false`) | | `enableCrashReporting` | `boolean` | No | Install the automatic error handler (default: `true`) — see [Error Tracking](/frameworks/angular/error-tracking) | | `onError` | `(error: unknown) => void` | No | Called if initialization itself fails (e.g. network unreachable) | ## 3. Verify the integration Inject `LerixService` and throw a test error to confirm everything is working: ```ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixService } from '@lerix-dev/lerix-angular'; @Component({ ... }) export class MyComponent { private readonly lerix = inject(LerixService); test() { this.lerix.throwError('Test error'); } } ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/angular/error-tracking) to report custom errors and enable automatic capture, or [Push Notifications](/frameworks/angular/push-notifications) to enable push delivery. # Angular — Push Notifications Source: https://docs.lerix.dev/frameworks/angular/push-notifications Configure standard Web Push (VAPID) delivery for your Angular app — no Firebase, no OneSignal required. ## Introduction Lerix delivers push notifications to Angular apps using the standard **Web Push protocol (VAPID)** — the same open standard Chrome, Firefox, Edge, and modern Safari all implement natively. No Firebase project, no OneSignal account — Lerix generates and manages the key pair for you. After you complete the [Angular installation](/frameworks/angular/installation), follow the steps below to enable push delivery. > [!NOTE] > > Make sure you have registered the SDK with `provideLerix()` before configuring push notifications. ## 1. Copy the service worker Web Push requires a service worker file served from your app's own origin — a package can't add this file to your app automatically. Copy it into your `public` folder so it's reachable at your site's root: ```bash theme={"dark"} cp node_modules/@lerix-dev/lerix-core/sw/lerix-sw.js your-app/public/lerix-sw.js ``` ## 2. Generate a Web Push key From the dashboard: **Notifications → Settings → Web Push → Generate keys**. This is a one-click step — Lerix generates and stores the VAPID key pair for the project; there's nothing to copy into your code. ## 3. Subscribe ```ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixService } from '@lerix-dev/lerix-angular'; @Component({ ... }) export class MyComponent { private readonly lerix = inject(LerixService); constructor() { this.lerix.notifications.setOnNotificationReceived((payload) => { // Fired while a tab is open. console.log(payload.title, payload.body); }); this.lerix.notifications.setOnNotificationTapped((payload) => { // Fired when the user clicks the notification — including one that // opened the page from a fully closed tab. const screen = payload.metadata['screen']; }); } async enablePush() { const subscribed = await this.lerix.notifications.subscribe(); } } ``` `subscribe()` prompts for notification permission, registers the service worker, subscribes this browser to push, and registers the subscription with the backend — one call for the whole flow. ## The notification payload Both callbacks receive a `LerixNotificationPayload`: ```ts theme={"dark"} interface LerixNotificationPayload { notificationId: string | null; title: string | null; body: string | null; metadata: Record; imageUrl: string | null; } ``` ## Device identifiers | Method | Returns | Use it for | | - | - | - | | `getRegisteredTokenId()` | Lerix's own id for this registered device | The value for the dashboard's/REST API's device-targeting field | | `getDeviceToken()` | The raw `PushSubscription`, stringified | Rarely needed directly; diagnostics only | | `getDeviceId()` | A locally generated identifier, unrelated to Lerix's backend | Local diagnostics only; **not** what the dashboard's send flow expects | ```ts theme={"dark"} const tokenId = this.lerix.notifications.getRegisteredTokenId(); ``` All three return `null` until `subscribe()` has completed at least once. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the page inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```ts theme={"dark"} this.lerix.notifications.setOnNotificationTapped((payload) => { const chatId = payload.metadata['chatId']; }); ``` ## Sending an image Set `imageUrl` when sending — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications). This works automatically with no extra setup: the service worker renders it directly as the notification's icon/image. > [!NOTE] > > **Custom sound is not supported for web push.** The Web Notifications API > doesn't expose a way to set a custom notification sound across browsers, > so the dashboard/API's `sound` parameter is ignored for browser > recipients — it only applies to iOS and Android. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```ts theme={"dark"} await this.lerix.setUser('user_123'); // after login await this.lerix.clearUser(); // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```ts theme={"dark"} await this.lerix.notifications.subscribeToTopic('promotions'); await this.lerix.notifications.unsubscribeFromTopic('promotions'); ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. ## Browser support Chrome, Firefox, and Edge support Web Push fully. Safari requires macOS 13+ or iOS 16.4+ — older Safari doesn't support the standard at all. # Release control in Angular Source: https://docs.lerix.dev/frameworks/angular/release-control Ask users to reload for a new deployment, require it, or show a maintenance screen in your Angular app. For what release control does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default once `provideLerix()` from [Installation](/frameworks/angular/installation) runs, with the built-in screens. Set `release` so update rules can compare versions: ```ts app.config.ts theme={"dark"} provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', release: '1.4.0', // required for update rules appControl: { enabled: true, ui: true }, }), ``` | Option | Default | Description | | - | - | - | | `release` | none | The version this deployment ships as. Without it, update rules never apply on the web. Use a version with digits, such as `1.4.0`, not a commit SHA. | | `appControl.enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `appControl.ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | ## What users see The same screens as in [React](/frameworks/react/release-control#what-users-see): a maintenance overlay with **Try again**, a required update that reloads the page at most once every 5 minutes and then shows a **Reload** button, and a dismissible bar for optional or recommended updates. ## Your own screens Set `appControl: { ui: false }` and inject `LerixAppControlService`. Its `state` is a signal and `state$` an observable of the same value: `update`, `maintenance` and `sdk`, each `null` when nothing applies (fields in [The state](/modules/release-control/custom-ui#the-state)). ```ts release-gate.component.ts theme={"dark"} import { Component, inject } from '@angular/core'; import { LerixAppControlService } from '@lerix-dev/lerix-angular'; @Component({ selector: 'app-release-gate', template: ` @if (appControl.state().maintenance; as maintenance) { } @else if (appControl.state().update?.state === 'required') { } @else { } `, }) export class ReleaseGateComponent { readonly appControl = inject(LerixAppControlService); } ``` | Member | What it does | | - | - | | `state` | Signal of the current state | | `state$` | Observable: the current state on subscribe, then every change | | `getState()` | The current state, read once | | `refresh()` | Checks now and resolves the new state. Never rejects. | | `openStore()` | Reloads the page | | `later()` | Hides the soft prompt for `remindAfterHours` | | `skip()` | Hides the soft prompt until a newer version (optional updates only) | # Angular — Source Maps Source: https://docs.lerix.dev/frameworks/angular/source-maps Upload your production source maps privately so Lerix shows Angular errors at the original file and line. ## Introduction Production Angular builds are minified, so a stack trace from a real user looks like this: ```text theme={"dark"} onConnMessage@https://app.example.com/chunk-Cr8GYWvZ.js:10:14780 ``` When you upload your build's source maps to Lerix, the dashboard maps each frame back to your original code, for example `src/app/chat/chat.page.ts:142`, and shows the surrounding lines. The maps are uploaded privately and deleted from your build output before you deploy. They are never served publicly, so your source code stays private. > [!NOTE] > > Complete the [Angular installation](/frameworks/angular/installation) first. > Setting a release requires `@lerix-dev/lerix-core` 1.1.0 or newer. ## 1. Enable hidden source maps In `angular.json`, turn on source maps for the production configuration under `projects..architect.build.configurations.production`: ```json angular.json theme={"dark"} "production": { "sourceMap": { "scripts": true, "styles": false, "hidden": true, "vendor": false } } ``` `"hidden": true` generates the `.map` files without adding a `//# sourceMappingURL` comment to the shipped JavaScript, so browsers never try to load them. Run `ng build` and check that `.js.map` files appear next to the bundles. The output folder is usually `dist//browser`. ## 2. Create a private API key Uploading source maps needs a private key, separate from the public project key your app uses. 1. Open your project in the [Lerix Dashboard](https://app.lerix.dev). 2. Go to **Project settings** → **API keys**. 3. Create a key with the **Upload source maps** permission (or **Full access**). > [!WARNING] > > This key is secret. Store it as a CI secret and never put it in client code > or commit it to your repository. ## 3. Upload the maps after each build After `ng build` and before you deploy, run the Lerix CLI on the build output: ```bash theme={"dark"} npx @lerix-dev/lerix-cli upload ``` Run it from your project folder. It finds the build output on its own (`angular.json` (`outputPath`)) and uses the `version` from `package.json` as the release. To set them yourself: `npx @lerix-dev/lerix-cli upload dist/my-app/browser --release 1.4.0`. The CLI uploads every `*.js.map` file in the folder (it skips `node_modules` and CSS maps), then deletes the `.map` files from the build output so they are never deployed. If any upload fails, it keeps the maps and exits with code 1, so you can retry the step. The CLI requires Node.js 18.17 or newer. ### Upload on every build Instead of running the command yourself, install the CLI in your project and let npm run it after each build: ```bash theme={"dark"} npm install --save-dev @lerix-dev/lerix-cli ``` ```json package.json theme={"dark"} "scripts": { "build": "ng build", "postbuild": "lerix upload --optional" } ``` Now every `npm run build` uploads the maps. `--optional` keeps local builds working: on a machine without `LERIX_SECRET_KEY` (a developer's laptop), the upload is skipped instead of failing the build, and the `.map` files are still deleted. Set the keys in your CI or build server, where the release build runs. > [!NOTE] > > pnpm and Yarn 2+ don't run `postbuild` scripts. Chain the command instead: > `"build": "ng build && lerix upload --optional"`. ### Options Each option can also be set with an environment variable. | Option | Environment variable | Description | | - | - | - | | `--release` | `LERIX_RELEASE` | The version this build ships as. Defaults to `version` in `package.json`. Pass the same value to the SDK (see step 4) | | `--api-key` | `LERIX_API_KEY` | Your normal public project API key | | `--project-id` | `LERIX_PROJECT_ID` | Your project ID | | `--secret-key` | `LERIX_SECRET_KEY` | The private key from step 2 | | `--url` | `LERIX_URL` | Lerix API URL. Defaults to `https://api.lerix.dev/v1` | | `--keep-maps` | | Keep the `.map` files in the build output instead of deleting them | | `--dry-run` | | List the maps that would be uploaded, without uploading anything | | `--optional` | | Skip the upload instead of failing when the keys aren't set (local builds). The `.map` files are still deleted | ## 4. Pass the same release to the SDK Set `release` in `provideLerix()` to the value you passed to `--release`: ```ts app.config.ts theme={"dark"} import { ApplicationConfig } from '@angular/core'; import { provideLerix } from '@lerix-dev/lerix-angular'; export const appConfig: ApplicationConfig = { providers: [ provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', release: '1.4.0', }), ], }; ``` Lerix matches maps by the script's file name. Angular's chunk names are content-hashed, so they are unique per build, and Lerix prefers maps from the same release. Matching still works if you omit `release`, but setting it is recommended. The release also replaces the `0.0.0` app version previously shown for web apps. > [!TIP] > > Use the same value in both places without editing it by hand: your git > commit SHA or the `version` from `package.json` both work well. ## Example: GitHub Actions This workflow builds the app, uploads the maps, then deploys. Add `LERIX_API_KEY`, `LERIX_PROJECT_ID` and `LERIX_SECRET_KEY` as repository secrets. ```yaml .github/workflows/deploy.yml theme={"dark"} jobs: deploy: runs-on: ubuntu-latest env: LERIX_RELEASE: ${{ github.sha }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci # Make the same release available to the app at build time - run: echo "export const release = '$LERIX_RELEASE';" > src/environments/release.ts - run: npx ng build - run: npx @lerix-dev/lerix-cli upload env: LERIX_API_KEY: ${{ secrets.LERIX_API_KEY }} LERIX_PROJECT_ID: ${{ secrets.LERIX_PROJECT_ID }} LERIX_SECRET_KEY: ${{ secrets.LERIX_SECRET_KEY }} # Deploy dist/my-app/browser here. It no longer contains .map files. ``` Then import the generated value in your config: ```ts app.config.ts theme={"dark"} import { release } from '../environments/release'; provideLerix({ apiKey: 'your-api-key', projectId: 'your-project-id', release }) ``` ## What you see in the dashboard New errors show the original file, line and function, with the surrounding code. Click **Show minified** to see the stack exactly as the browser sent it. The AI assistant, GitHub issues and the MCP server all receive the readable stack too. * Only errors reported after the maps are uploaded are mapped. Earlier errors keep their minified stack. * Lerix keeps the newest 5,000 maps per project. * Each map can be up to 50 MB. ## Troubleshooting | Problem | Fix | | - | - | | `no .js.map files found` | Source maps are not enabled for the production build. Check the `sourceMap` setting in step 1 and that you point the CLI at the right output folder. | | `Uploading source maps needs a private API key...` (`SOURCE_MAP_SECRET_KEY_MISSING`) | `--secret-key` is missing or invalid. Create a private key as described in step 2. | | `PROJECT_API_KEY_DONT_HAVE_PERMISSION` | The private key lacks the **Upload source maps** permission. Edit the key or create a new one with that permission. | | The stack is still minified | Check that the SDK `release` matches `--release`, that you uploaded maps from this exact build, and that the error is new. Every rebuild changes chunk hashes, so upload the maps again after each build. | # ASP.NET Core — Error Tracking Source: https://docs.lerix.dev/frameworks/aspnet/error-tracking Automatic reporting of unhandled exceptions in ASP.NET Core, plus manual reporting with type, severity and metadata. ## Introduction After you complete the [ASP.NET Core installation](/frameworks/aspnet/installation), unhandled exceptions are reported automatically. Inject `LerixClient` to report handled exceptions yourself with extra context. ## Automatic capture `AddLerix` and `UseLerix` register two things: * **A middleware.** Every exception that escapes the middleware after it (routing, authentication, your endpoints) passes through it. By default it reports every exception except those that carry a status below 500, such as a `BadHttpRequestException` with status 400. The middleware then rethrows, so `UseExceptionHandler` or the developer exception page builds exactly the response it would have built without Lerix. Requests the client aborted are not reported. * **Process-level handlers.** `AppDomain.UnhandledException` and `TaskScheduler.UnobservedTaskException` are reported as `Crash` with `Critical` severity. Set `CaptureUnhandled = false` to skip them. Every automatic report carries the request method, URL, route pattern, status code, and the `X-Request-Id` header (or ASP.NET's `TraceIdentifier`) under `metadata.request`. ### Change what is reported `ShouldCapture` receives the exception, the `HttpContext` and the status that will be sent when it is known: ```csharp Program.cs theme={"dark"} builder.Services.AddLerix(o => { o.IgnorePaths.Add("/health"); o.ShouldCapture = (exception, context, status) => { if (exception is OperationCanceledException) return false; return status is null or >= 500; }; }); ``` `IgnorePaths` is a shortcut for skipping whole route prefixes without a delegate. ### Middleware order `UseLerix` must sit after `UseExceptionHandler` (which would otherwise catch the exception first) and before the middleware you want covered. A typical pipeline: ```csharp Program.cs theme={"dark"} app.UseExceptionHandler("/error"); app.UseLerix(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); ``` ## Filtering errors Requires `Lerix` / `Lerix.AspNetCore` 0.4.0 or later. Three `LerixOptions` properties decide what leaves the process. They apply to every report, whatever its origin: the ASP.NET Core middleware, the process-level handlers, and `CaptureException` / `CaptureMessage`. Use them for message-based filtering; use `ShouldCapture` (above) to decide by exception, request or status. ```csharp Program.cs theme={"dark"} builder.Services.AddLerix(o => { // A string matches when it appears anywhere in the message (case-insensitive, blank entries are skipped). o.IgnoreErrors.Add("The client reset the request stream"); // A Regex matches when it finds a match in the message. o.IgnoreErrorPatterns.Add(new Regex("^TaskCanceledException")); // Runs after the ignore lists. Return the event (changed or not) to send it, or null to drop it. o.BeforeSend = e => { if (e.Metadata != null && e.Metadata.TryGetValue("tenant", out var t) && (string?)t == "load-test") return null; e.Message = Regex.Replace(e.Message, @"\b\d{6,}\b", ""); return e; }; }); ``` `IgnoreErrors` can also be bound from `appsettings.json`: ```json appsettings.json theme={"dark"} { "Lerix": { "IgnoreErrors": ["The client reset the request stream"] } } ``` * `IgnoreErrors` (`IList`) and `IgnoreErrorPatterns` (`IList`) run first, then `BeforeSend` (`Func`) sees only what's left. * The message matched and passed to `BeforeSend` is the issue title shown in the dashboard, for example `InvalidOperationException: boom`. * `LerixErrorEvent` has `Message`, `Stack` (`at ...` lines), `Type` (`BugType`), `Severity` (`BugSeverity`) and `Metadata` (what you passed to the capture call, or the request context from the middleware). Device, app and runtime details are added after the hook, which runs on the thread that captured the error. * If `BeforeSend` throws, the original event is sent and the exception is only logged. It is never reported or rethrown. * A dropped report does not pin its request's trace. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). ## Report an error manually `LerixClient` is a singleton in the container: ```csharp OrdersService.cs theme={"dark"} using Lerix; public class OrdersService(LerixClient lerix, ILedger ledger) { public async Task Reconcile(string orderId) { try { await ledger.Reconcile(orderId); } catch (Exception error) { await lerix.CaptureException(error, new CaptureOptions { Type = BugType.LogicBug, Severity = BugSeverity.High, Metadata = new Dictionary { ["orderId"] = orderId, ["job"] = "reconcile" }, }); throw; } } } ``` `CaptureException` never throws and completes once the report has been accepted or given up on. You can also discard the task with `_ =` to keep going without waiting. Use `CaptureMessage` for something that was not thrown: ```csharp theme={"dark"} await lerix.CaptureMessage("Inventory drift detected", new CaptureOptions { Severity = BugSeverity.Medium, Metadata = new Dictionary { ["sku"] = "A-1", ["expected"] = 10, ["actual"] = 7 }, }); ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `error` / `message` | `Exception` / `string` | Yes | The caught exception, or a message | | `Type` | `BugType` | No | Bug classification (default `RuntimeError` for exceptions, `LogicBug` for messages) | | `Severity` | `BugSeverity` | No | Severity level (default `High` for exceptions, `Medium` for messages) | | `Metadata` | `IDictionary` | No | Additional JSON context attached to the report | ### `BugType` values | Value | Description | | - | - | | `RuntimeError` | An unhandled exception thrown at runtime | | `LogicBug` | Incorrect behavior that doesn't throw | | `UiBug` | A visual or layout defect | | `NetworkError` | A failed or malformed network request | | `Performance` | Slowness or excessive resource use | | `Compatibility` | Breaks on a specific runtime, OS or platform | | `ValidationError` | Invalid input passed validation it shouldn't have | | `Security` | A security-relevant defect | | `Crash` | The process terminated unexpectedly | | `Unknown` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `Critical` | Blocks core functionality or affects all users | | `High` | Serious impact, but a workaround exists | | `Medium` | Noticeable but limited impact | | `Low` | Minor or cosmetic | | `Unknown` | Severity not yet assessed | ## What is attached to a report | Field | Value | | - | - | | Device | Machine name, CPU architecture, OS name and version, time zone | | App | Service name and id from the entry assembly (or the `App` option), version and build number | | Metadata | `environment`, .NET runtime description and process id, plus anything you pass in `Metadata` | Stack traces include inner exceptions, each introduced by a `--- caused by ... ---` line. ## File and line numbers .NET only puts file and line numbers in stack traces when your app's `.pdb` files are available at runtime. Add a `Directory.Build.props` file at the root of your repository: ```xml Directory.Build.props theme={"dark"} embedded $(MSBuildThisFileDirectory)=/_/ ``` The SDK strips the deterministic `/_/` root, so this frame: ```text theme={"dark"} at Api.Orders.Find() in /_/src/Api/Orders.cs:line 42 ``` is reported as `src/Api/Orders.cs:line 42`. A `ContinuousIntegrationBuild` build produces the same `/_/` root. If stack traces arrive with no file or line at all, the SDK logs one warning per process that points to this page. ## Other `LerixClient` members | Member | Description | | - | - | | `GetUserId()` | Anonymous user id this service is registered as | | `FlushAsync(timeout?)` | Wait for in-flight reports; the hosted service calls it on shutdown | | `DeleteUserAsync()` / `ReRegisterUserAsync()` | Reset the anonymous user, for example after pointing the service at a different project | | `InstallProcessHandlers()` | Install the process-level handlers yourself when not using the hosted service | ## Disable in tests Set `Enabled = false` to turn every call into a no-op without changing your code: ```csharp theme={"dark"} builder.Services.AddLerix(o => o.Enabled = !builder.Environment.IsEnvironment("Testing")); ``` # ASP.NET Core — Analytics and Feedback Source: https://docs.lerix.dev/frameworks/aspnet/feedback-analytics Track server-side events in the same funnels as your apps, and send feedback from your own backend to the Feedback inbox. Requires `Lerix` 0.4.0 or later. Both features use the `LerixClient` singleton you inject after [Installation](/frameworks/aspnet/installation). For what you see in the dashboard, read [Analytics](/modules/analytics) and [Feedback](/modules/feedback). ## Analytics Record what happens on the server: sign-ups, payments, jobs. Events show up under **Analytics** in the dashboard next to the ones your apps send. ```csharp OrdersService.cs theme={"dark"} public class OrdersService(LerixClient lerix) { public void Complete(Order order, User user) { // ... lerix.Track("checkout_completed", new Dictionary { ["items"] = 3, ["total"] = 49.90m, ["coupon"] = "EID", }, userId: user.Id); } } ``` * `Track` never throws and never blocks on the network. It validates the event and puts it in an in-memory buffer. * `userId` is your ID for the user the event is about, the same one your apps pass to [`setUser()`](/modules/users-identity) (trimmed, up to 255 characters). It is sent per event, so one service can track many users. * **Names** start with a letter, then letters, digits, spaces, `_`, `.`, `:` or `-` (2 to 64 characters). Any other name drops the event. * **Props**: up to 25 keys, each starting with a letter or `_`, then letters, digits, `_`, `.` or `-`. Values are strings (up to 1,024 characters), finite numbers (any numeric type or `decimal`), `bool` or `null`; a `JsonElement` of one of those kinds works too. Anything else (nested objects, lists, dates, NaN) is dropped key by key, and logged when `Debug` is on. ### Delivery Events are sent every `FlushInterval` (10 seconds), as soon as `FlushAt` (50) are waiting, on `FlushAsync()`, and on shutdown (`IHostedService.StopAsync`, or disposing the client). Each request carries at most 100 events. The buffer holds `MaxQueueSize` (1,000) events, dropping the oldest when full. Nothing is written to disk. When the API is unreachable or returns a retryable error, the batch is kept and sending backs off from 2 seconds up to 5 minutes. Batches the API rejects as invalid are dropped. If the project has analytics paused on the Lerix side, sending waits for the time the API asks for. ```csharp theme={"dark"} await lerix.Analytics.FlushAsync(); // also part of lerix.FlushAsync() lerix.Analytics.SetEnabled(false); // stop recording and clear the buffer (until restart) ``` ```json appsettings.json theme={"dark"} { "Lerix": { "Analytics": { "FlushInterval": "00:00:05", "FlushAt": 20 } } } ``` `"Analytics": { "Enabled": false }` turns `Track` into a no-op. Events carry the environment from the `Environment` option (or `ASPNETCORE_ENVIRONMENT` / `DOTNET_ENVIRONMENT`), lowercased, with `Production` sent as `prod` and `Development` as `dev`. ## Feedback Send user feedback from your own UI, a support inbox or a bot. It shows up under **Feedbacks** in the dashboard, like feedback sent from the app SDKs. ```csharp theme={"dark"} var id = await lerix.Feedback.SubmitAsync(new LerixFeedback { Message = "The export button does nothing", Email = "user@example.com", Labels = new[] { "bug" }, UserId = user.Id, Custom = new Dictionary { ["plan"] = "pro" }, Attachments = new[] { new LerixFeedbackAttachment(screenshotBytes) { Width = 1280, Height = 800 } }, }); ``` * `Message` is required (1 to 4,000 characters after trimming). `Labels` are the label keys set up for the project; unknown ones are dropped by the API. * `Custom` takes up to 50 keys and 16 KB of JSON. * `Attachments`: up to 5 PNG, JPEG or WebP images of at most 5 MB each. The type is read from the bytes; a `ContentType` that disagrees with them is an error. * A call made inside a [traced request](/frameworks/aspnet/request-tracing) carries that request's trace ID, and the trace is kept. * Invalid input throws `LerixApiException` with `Code = "INVALID_FEEDBACK"` before anything is sent. Network errors, 5xx and 429 are retried twice with the same client ID, so a retry never creates a second entry. The last error is thrown as `LerixApiException`. * Returns the feedback ID, or `null` when the SDK is disabled (`Enabled = false`). > [!NOTE] > > The `UserId` you pass is stored as *reported* by your server. Replies by push > only go to users verified through a client SDK's `setUser`. # ASP.NET Core — Installation Source: https://docs.lerix.dev/frameworks/aspnet/installation Add the Lerix .NET SDK to your ASP.NET Core backend to report unhandled exceptions and send push notifications from your server. ## Introduction `Lerix.AspNetCore` is the server-side Lerix SDK for ASP.NET Core. After two lines in `Program.cs`, every unhandled exception in your API is reported to the dashboard with its stack trace, host, release and request context, and your services get a typed client for the push notifications REST API. > [!NOTE] > > Create a project with the **ASP.NET** framework in the [dashboard](https://app.lerix.dev) > and copy its **API key** and **project id** from **Project Settings**. ## Requirements | | Minimum | | - | - | | .NET | 8.0 | | ASP.NET Core | 8.0 | The `Lerix` core package also targets .NET Standard 2.0 for console apps, workers and functions that do not use ASP.NET Core. ## 1. Install the package ```bash theme={"dark"} dotnet add package Lerix.AspNetCore ``` ## 2. Register the SDK Add the services and the middleware in `Program.cs`: ```csharp Program.cs theme={"dark"} using Lerix.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddLerix(builder.Configuration.GetSection("Lerix")); var app = builder.Build(); app.UseExceptionHandler("/error"); app.UseLerix(); app.MapControllers(); app.Run(); ``` Put the options in `appsettings.json` or, for the secrets, in user secrets or environment variables: ```json appsettings.json theme={"dark"} { "Lerix": { "ApiKey": "YOUR_PROJECT_API_KEY", "ProjectId": "YOUR_PROJECT_ID", "Environment": "production" } } ``` `ApiKey` and `ProjectId` also fall back to the `LERIX_API_KEY` and `LERIX_PROJECT_ID` environment variables, so the section can be omitted entirely on a server that already sets them. That is the whole setup. `AddLerix` registers a hosted service that connects to your project on startup and flushes pending reports on shutdown, and `UseLerix` adds the middleware that reports unhandled exceptions. See [Error Tracking](/frameworks/aspnet/error-tracking) for what gets captured and how to report errors yourself. > [!WARNING] > > Call `app.UseLerix()` **after** `UseExceptionHandler` or > `UseDeveloperExceptionPage` and **before** routing and endpoints. Those > handlers catch exceptions from the middleware that follows them, so if > Lerix is registered earlier it never sees the exception. ### Configure in code `AddLerix` also accepts a delegate, alone or on top of a configuration section: ```csharp Program.cs theme={"dark"} builder.Services.AddLerix(o => { o.ApiKey = builder.Configuration["LERIX_API_KEY"]; o.ProjectId = builder.Configuration["LERIX_PROJECT_ID"]; o.Environment = builder.Environment.EnvironmentName; o.IgnorePaths.Add("/health"); }); ``` ## 3. Verify the integration Add an endpoint that throws and call it once: ```csharp Program.cs theme={"dark"} app.MapGet("/lerix-test", () => { throw new InvalidOperationException("Lerix test error"); }); ``` The client receives your app's normal `500` response and the error appears in your [dashboard](https://app.lerix.dev) within seconds. Remove the endpoint afterwards. ## Options Every option is a property of `LerixAspNetCoreOptions`, settable from configuration or the delegate. | Option | Default | Description | | - | - | - | | `ApiKey` | `LERIX_API_KEY` | Project API key | | `ProjectId` | `LERIX_PROJECT_ID` | Project id | | `Url` | `https://api.lerix.dev/v1` | API base URL (`LERIX_API_URL`) | | `Environment` | `LERIX_ENVIRONMENT`, then `ASPNETCORE_ENVIRONMENT` | Sent as metadata with every report | | `App` | from the entry assembly | `AppInfo { Id, Name, Version, BuildNumber }` used to identify this service | | `Enabled` | `true` | `false` turns the SDK into a no-op, useful in tests | | `Debug` | `false` | Log requests and reports through `ILogger` | | `CaptureUnhandled` | `true` | Report `AppDomain.UnhandledException` and `TaskScheduler.UnobservedTaskException` | | `ShouldCapture` | 5xx only | `(exception, httpContext, status) => bool` deciding what the middleware reports | | `IgnorePaths` | empty | Request path prefixes whose exceptions are never reported | | `PersistState` | `true` | Keep the anonymous user id on disk between restarts | | `StatePath` | `.lerix/state.json` | Where that id is stored, relative to the working directory | | `UserId` | | Pin the anonymous user id instead of registering one | | `FlushTimeout` | `00:00:02` | How long shutdown waits for pending reports | | `Timeout` | `00:00:10` | HTTP timeout per request | ## How your service appears in the dashboard The SDK registers your service as a `server` app. The app id defaults to the entry assembly name and the release to its informational version (the `` in your `.csproj`), so reports are grouped per service and per release. Override them with `App` when you deploy several instances of the same code base or want a git SHA as the build number: ```csharp Program.cs theme={"dark"} builder.Services.AddLerix(o => { o.App = new AppInfo { Id = "orders-api", Version = version, BuildNumber = gitSha }; }); ``` An anonymous user is registered on first boot and stored in `.lerix/state.json` under the working directory, so restarts reuse it. Add that folder to `.gitignore`, or mount it in containers. Set `PersistState = false` to skip persistence, or `UserId` to control the id yourself. ## Outside ASP.NET Core Console apps, workers and functions can use the `Lerix` package directly: ```bash theme={"dark"} dotnet add package Lerix ``` ```csharp theme={"dark"} using Lerix; using var lerix = new LerixClient("YOUR_PROJECT_API_KEY", "YOUR_PROJECT_ID"); lerix.InstallProcessHandlers(); await lerix.InitializeAsync(); try { await DoWork(); } catch (Exception error) { await lerix.CaptureException(error); throw; } finally { await lerix.FlushAsync(); } ``` ## Next steps > **[Error Tracking](/frameworks/aspnet/error-tracking)** > > What the middleware reports, how to change the policy, and manual reporting. > **[Push Notifications](/frameworks/aspnet/push-notifications)** > > Send, schedule, update and unsend notifications from your backend. # ASP.NET Core — Push Notifications Source: https://docs.lerix.dev/frameworks/aspnet/push-notifications Send, schedule, update and unsend push notifications from your ASP.NET Core backend with a typed client for the Lerix REST API. ## Introduction A backend does not receive push notifications, it sends them. The .NET SDK wraps the [push notifications REST API](/api-reference/push-notifications) in a typed client so your services can notify the users of your mobile and web apps without hand-writing HTTP calls. ## 1. Create a private key Notifications are sent to the devices of the project your **app** uses, which is usually a different project from the one your backend reports errors to. In that project's **Project Settings**, create a **private key** with the *send push notifications* permission. > [!WARNING] > > A private key can send notifications to all of a project's users. Keep it > in user secrets or an environment variable on the server and never ship it > to a client. ## 2. Register the client ```csharp Program.cs theme={"dark"} builder.Services.AddLerixNotifications(builder.Configuration.GetSection("LerixNotifications")); ``` ```json appsettings.json theme={"dark"} { "LerixNotifications": { "ProjectId": "MOBILE_PROJECT_ID", "ApiKey": "MOBILE_PROJECT_PRIVATE_KEY" } } ``` A delegate works too, and `ProjectId` / `ApiKey` fall back to `LERIX_NOTIFICATIONS_PROJECT_ID` / `LERIX_NOTIFICATIONS_API_KEY`. The client is registered as a `LerixNotifications` singleton and is independent of `AddLerix`, so a service can send notifications without reporting errors. ## 3. Send a notification ```csharp OrdersService.cs theme={"dark"} using Lerix; public class OrdersService(LerixNotifications push) { public async Task NotifyShipped(User user) { var result = await push.SendAsync(new SendNotificationInput { Title = "Order shipped", Body = "Your order #1234 is on its way", DeviceTokens = new[] { user.LerixDeviceId }, Metadata = new Dictionary { ["orderId"] = "1234" }, }); return result.NotificationId; } } ``` `DeviceTokens` are the Lerix device ids your client apps obtain from `getDeviceId()` and send to your backend at login. Save the returned `NotificationId`, every other method takes it. ### Broadcast to a topic ```csharp theme={"dark"} await push.SendAsync(new SendNotificationInput { Title = "Weekend sale", Body = "20% off everything until Sunday", SendByTopic = true, Topic = "promotions", }); ``` ### Schedule for later ```csharp theme={"dark"} await push.SendAsync(new SendNotificationInput { Title = "Reminder", Body = "Your appointment is in one hour", DeviceTokens = new[] { deviceId }, SentAt = appointment.AddHours(-1), }); ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `Title` | `string` | Yes | Notification title | | `Body` | `string` | Yes | Notification body | | `DeviceTokens` | `IList` | Unless `SendByTopic` | Lerix device ids to deliver to | | `SendByTopic` | `bool` | No | Deliver to every device subscribed to `Topic` instead | | `Topic` | `string` | When `SendByTopic` | Topic key | | `Sound` | `string` | No | Custom sound file bundled in the client app | | `ImageUrl` | `string` | No | Public `https://` image shown as a rich image | | `Metadata` | `IDictionary` | No | Delivered to the client as `payload.metadata` | | `SentAt` | `DateTimeOffset?` | No | When to deliver; sent as ISO 8601 UTC | Platform notes for `Sound` and `ImageUrl` are in the [REST API reference](/api-reference/push-notifications). ## 4. Manage a sent notification ```csharp theme={"dark"} // Change the content. Scheduled ones are edited in place; sent ones are redelivered. await push.UpdateAsync(notificationId, "Delivered", "Enjoy your order!"); // Remove an already-delivered notification from devices. await push.UnsendAsync(notificationId); // Stop a scheduled notification before it goes out. await push.CancelAsync(notificationId); // Per-recipient delivery outcome as reported by APNs, FCM or the browser push service. var deliveries = await push.DeliveriesAsync(notificationId); ``` | Method | REST endpoint | | - | - | | `SendAsync(input)` | [`POST /notifications/send`](/api-reference/push-notifications) | | `UpdateAsync(id, title, body)` | [`POST /notifications/{id}/update`](/api-reference/update-notification) | | `UnsendAsync(id)` | [`POST /notifications/{id}/unsend`](/api-reference/unsend-notification) | | `CancelAsync(id)` | [`POST /notifications/{id}/cancel`](/api-reference/cancel-notification) | | `DeliveriesAsync(id)` | [`GET /notifications/{id}/deliveries`](/api-reference/notification-deliveries) | `UpdateAsync`, `UnsendAsync` and `CancelAsync` return the raw JSON response as a `JsonElement?`; `DeliveriesAsync` returns one `JsonElement` per recipient. ## Errors Every method throws `LerixApiException` when the API rejects the request. `Code` is the backend's error code and `Status` the HTTP status: ```csharp theme={"dark"} using Lerix; try { await push.CancelAsync(id); } catch (LerixApiException error) when (error.Code == "CANNOT_CANCEL_SENT_NOTIFICATION") { // already delivered } ``` Invalid input such as a missing `Topic` with `SendByTopic` is rejected locally with code `INVALID_INPUT` before any request is made. The full list of codes is in [Error & success codes](/developers/errors-responses). ## Several target projects Instantiate `LerixNotifications` directly when one backend serves more than one app: ```csharp theme={"dark"} var rider = new LerixNotifications("rider-app", config["RIDER_PRIVATE_KEY"]!); var driver = new LerixNotifications("driver-app", config["DRIVER_PRIVATE_KEY"]!); ``` # ASP.NET — Request Tracing Source: https://docs.lerix.dev/frameworks/aspnet/request-tracing One span per request from ASP.NET Core's own activity, HttpClient and database spans, sampling, and the trace behind every error. ## Introduction After the [installation](/frameworks/aspnet/installation), turn on request tracing to see, under **Requests** in the dashboard, every request your service handled with its route, duration, database commands and outbound calls — and to open the exact request behind any error. See [Request Tracing](/modules/request-tracing) for what the dashboard shows. ## Enable it ```csharp Program.cs theme={"dark"} builder.Services.AddLerix(o => { o.ApiKey = "..."; o.ProjectId = "..."; o.Tracing.Enabled = true; }); var app = builder.Build(); app.UseExceptionHandler("/error"); app.UseLerix(); ``` Or from configuration: ```json appsettings.json theme={"dark"} { "Lerix": { "ApiKey": "...", "ProjectId": "...", "Tracing": { "Enabled": true } } } ``` Tracing is off by default; when off, no activity listener is registered and nothing changes for the application. It is built on `System.Diagnostics.Activity`, so it needs no extra packages. ## What is recorded * **One server span per request**, taken from ASP.NET Core's own request activity and named after the **route template** (`GET /users/{id:int}`), never the raw path. The framework continues an incoming W3C `traceparent` header; `UseLerix()` completes the span with route, status and client address. Only requests that pass through `UseLerix()` are traced. * **`HttpClient` calls** become `client` spans named `METHOD host` and carry `traceparent` to the next service. Calls to the Lerix API itself are ignored. * **Database commands** from drivers that publish activities — Npgsql, Microsoft.Data.SqlClient, MySqlConnector, and EF Core on top of them — become `client` spans with the SQL reduced to a template (`SELECT * FROM users WHERE id = ?`); parameters and rows are never recorded. Each source can be switched off, and your own `ActivitySource`s added: ```csharp theme={"dark"} o.Tracing.Instrumentations.HttpClient = true; o.Tracing.Instrumentations.Database = true; o.Tracing.Instrumentations.AdditionalActivitySources = new[] { "MyCompany.Cache" }; ``` Anything else by hand: ```csharp theme={"dark"} using (LerixTracing.StartChild("cache get", ActivityKind.Client, new Dictionary { ["server.address"] = "redis" })) { await cache.GetAsync(key); } ``` Outside a request `StartChild` returns `null`, and `using` on `null` is a no-op. ## The current trace The trace id is `Activity.Current`'s, for example for your own logs: ```csharp theme={"dark"} logger.LogInformation("handling order, trace {Trace}", Activity.Current?.TraceId.ToHexString()); ``` ## Sampling | Option | Default | Description | | - | - | - | | `Tracing.SampleRate` | `0.1` | Share of successful requests kept in full. Decided once at the root and carried in the W3C trace flags. | | `Tracing.SlowThreshold` | `2 s` | A request slower than this is always kept in full. | | `Tracing.MaxSpansPerRequest` | `1000` | Child spans held per request until it ends. | Failed requests and requests that reported an error are always kept whole. Every request's root span is still sent, so the per-route counts and percentiles in the dashboard cover 100% of traffic. ## Errors and traces Every error report made inside a request — the middleware's automatic reports as well as your own `CaptureException` / `CaptureMessage` calls — carries that request's trace, so the issue page shows the waterfall inline with the failing command highlighted. An error reported outside any request records as before, with no trace attached. ## Client addresses Each request span carries the client's address, resolved the way the project asks under **Project settings → Tracing**: in the default `masked` mode the SDK zeroes the last IPv4 octet or truncates IPv6 to its /64 before anything is sent; in `hashed` and `full` modes the address is handled on the Lerix side. The trusted-proxy depth and header decide how it is read from `X-Forwarded-For`; the whole chain is never trusted. ## Exporter options | Option | Default | Description | | - | - | - | | `Tracing.FlushInterval` | `5 s` | How often queued spans are sent when the batch is not full | | `Tracing.MaxQueueSize` | `2048` | Spans held in memory before new ones are dropped | | `Tracing.MaxBatchSize` | `512` | Spans per export request | | `Tracing.RequestTimeout` | `10 s` | Timeout of one export request | Spans leave as OTLP/HTTP JSON from the thread pool. If the Lerix API is unreachable the batch is counted and dropped; nothing is retried, nothing blocks a request, nothing throws into your code. Counters are on `lerixClient.Tracing?.Stats()`. # Edge Functions — Error Tracking Source: https://docs.lerix.dev/frameworks/edge/error-tracking What lerix.wrap() reports from your edge function, the error response, unhandled errors, and manual reporting. ## Introduction After the [installation](/frameworks/edge/installation), errors reach the dashboard in three ways: thrown out of a wrapped handler, escaping every handler as an unhandled error, or reported by you. ## Errors thrown by the handler `lerix.wrap(handler)` catches what the handler throws, reports it, and answers in its place: | Thrown | Response | Reported as | | - | - | - | | A plain error | `500` `{ "statusCode": 500, "message": "Internal server error" }` | `crash`, critical | | An error with a `5xx` `status` / `statusCode` / `getStatus()` | that status | `runtime_error`, high | | An error with a `4xx` status (e.g. Hono's `HTTPException(404)`) | that status, with the error's message | not reported | Each report carries the request: method, URL **without the query string**, route, status and a request id (`x-request-id`, `x-correlation-id`, `cf-ray` or `x-vercel-id`). Edge functions have no router to ask for a route, so value-like path segments are replaced: `/orders/8812/items` is reported as `/orders/:id/items`. The error is still printed with `console.error`, so it keeps showing up in your platform's logs. Pass `logErrors: false` to stop that. ### Customize the response ```ts theme={"dark"} Deno.serve(lerix.wrap(handler, { onError: (error, request) => Response.json({ error: 'Something went wrong' }, { status: 500 }), })); ``` ### Choose what is reported ```ts theme={"dark"} Lerix.init({ // ... shouldCapture: (error, request) => request.route !== '/health', captureServerErrors: true, // also report a 5xx the handler returns instead of throwing }); ``` ## Unhandled errors Errors that escape every handler, such as a rejected promise nobody awaited, are reported as `crash` through the runtime's `error` and `unhandledrejection` events (Supabase/Deno, Cloudflare Workers, Vercel Edge). Supabase ends the worker on such an error, normally before a report could be sent. So the SDK holds the runtime's default off until the report is out, then rethrows the error and the worker ends as it would have. Pass `exitOnUnhandled: false` to keep the worker serving instead. On Supabase, queued reports are also sent on `beforeunload`, just before an isolate shuts down. Set `captureUnhandled: false` to turn all of this off. ## Report errors yourself ```ts theme={"dark"} try { await chargeCard(order); } catch (error) { lerix.captureException(error, { severity: BugSeverity.CRITICAL, metadata: { orderId: order.id } }); return Response.json({ error: 'payment failed' }, { status: 402 }); } lerix.captureMessage('Stripe webhook arrived out of order', { type: BugType.LOGIC_BUG }); ``` Inside a wrapped handler, the current request is attached automatically. That needs `AsyncLocalStorage`: Supabase, Deno, Bun and Cloudflare with `nodejs_compat` provide it, and on Vercel Edge it is used when the runtime exposes a global one. Errors thrown out of the handler carry the request either way. Both methods never throw. Outside `wrap()` (a cron-style function, a queue consumer), hand the flush to the platform so the report is not lost when the isolate stops: ```ts theme={"dark"} EdgeRuntime.waitUntil(lerix.flush()); // Supabase ctx.waitUntil(lerix.flush()); // Cloudflare ``` ## Filter and edit reports `ignoreErrors` and `beforeSend` work as in every other Lerix SDK: ```ts theme={"dark"} Lerix.init({ // ... ignoreErrors: ['AbortError', /^NetworkError/], beforeSend: (event) => { if (event.message.includes('password')) return null; // drop return { ...event, metadata: { ...event.metadata, region: Deno.env.get('SB_REGION') } }; }, }); ``` A string matches anywhere in the message (case-insensitive); a `RegExp` matches when it finds a match. `beforeSend` runs after `ignoreErrors`; return `null` to drop the report. If it throws, the original report is sent. ## When reports are sent `wrap()` hands the sending to the first `waitUntil` it finds, so the response is not delayed: | Runtime | `waitUntil` | | - | - | | Supabase Edge Functions | `EdgeRuntime.waitUntil`, automatic | | Cloudflare Workers | the handler's `ctx`, automatic | | Next.js middleware | the handler's `event`, automatic | | Vercel edge routes | pass `{ waitUntil }` from `@vercel/functions` | | Deno, Bun servers | none: the response waits for the report, up to `flushTimeoutMs` | ## Limits * Errors thrown while a **streamed** response body is being read happen after `wrap()` has returned, so it does not see them. Catch them in the stream and call `captureException`. * Stack traces from Deno point at your TypeScript files directly. Bundled Workers and Vercel builds show bundle positions. # Edge Functions — Analytics and Feedback Source: https://docs.lerix.dev/frameworks/edge/feedback-analytics Track events from Supabase Edge Functions, Cloudflare Workers, Vercel Edge, Deno and Bun, and send feedback to the Feedback inbox. Requires `@lerix-dev/lerix-edge` 0.2.0 or later. Both features use the `lerix` client from [Installation](/frameworks/edge/installation). For what you see in the dashboard, read [Analytics](/modules/analytics) and [Feedback](/modules/feedback). ## Analytics ```ts theme={"dark"} Deno.serve(lerix.wrap(async (req) => { const { userId, amount } = await req.json(); lerix.track('payment_succeeded', { amount, currency: 'OMR' }, { userId }); return new Response('ok'); })); ``` * Events land in the same funnels and user counts as your apps' events. `userId` is your own ID for the user, the same one your apps pass to [`setUser()`](/modules/users-identity). * Names start with a letter, then letters, digits, spaces, `_`, `.`, `:` or `-` (2 to 64 characters). Up to 25 properties, with string (up to 1,024 characters), finite number, boolean or `null` values. An invalid name drops the event; an invalid property is dropped on its own. `track()` never throws. ### Delivery Events are buffered in the isolate (up to 1,000, oldest dropped first) and sent in batches of up to 100, after the response through `waitUntil`, or as soon as 50 are waiting. There is no timer, because isolates freeze between requests. Outside `wrap()`, for example in a cron handler or a queue consumer, flush before you return: ```ts theme={"dark"} lerix.track('nightly_sync_finished', { rows }); await lerix.flush(); ``` If the API is unreachable or answers 401, 429 or 5xx, the events stay buffered and a later request retries after a backoff (2 seconds up to 5 minutes). A batch refused as invalid is dropped. Nothing outlives the isolate. ### Environment and opt-out Events carry `platform: "edge"`, the runtime as the OS, your app's version and build number, and the environment: the `environment` option, or `NODE_ENV` where it can be read, with `production` sent as `prod` and `development` as `dev` (`prod` when unset). ```ts theme={"dark"} lerix.analytics.setEnabled(false); // stop recording and clear the buffer ``` `analytics: false` in the `Lerix.init` options turns `track()` into a no-op. ## Feedback ```ts theme={"dark"} const id = await lerix.feedback.submit({ message: 'Checkout button does nothing', email: 'user@example.com', // optional labels: ['bug'], // project label keys userId, // optional, your ID for the user custom: { plan: 'pro' }, // optional, up to 50 keys / 16 KB attachments: [{ data: pngBytes }], // optional, up to 5 PNG/JPEG/WebP images of 5 MB }); ``` * Attachments are uploaded first, then the feedback is created with their IDs. * Failed requests are retried twice with the same client ID, so a retry never duplicates the feedback. * `submit()` rejects with `LerixApiException` (`INVALID_FEEDBACK` for input that can't be sent, or the API's error) and resolves `null` with `enabled: false`. * Inside a [traced request](/frameworks/edge/request-tracing), the feedback links to the trace. > [!NOTE] > > The `userId` you pass is stored as *reported* by your server. Replies by push > only go to users verified through a client SDK's `setUser`. # Edge Functions — Installation Source: https://docs.lerix.dev/frameworks/edge/installation Add Lerix to Supabase Edge Functions, Cloudflare Workers, Vercel Edge, Deno or Bun to report errors, trace requests and send push notifications. ## Introduction `@lerix-dev/lerix-edge` is the Lerix SDK for edge runtimes: **Supabase Edge Functions**, Cloudflare Workers, Vercel and Next.js edge routes, Netlify Edge, Deno Deploy and Bun. Wrap your handler once and every error it throws is reported to the dashboard with its stack trace and request. The same package can trace requests and send push notifications to your users. It uses Web-standard APIs only (`fetch`, Web Crypto) and has no dependencies. Nothing is sent until the first report, and reports go out after the response through the platform's `waitUntil`, so a healthy function pays nothing. > [!NOTE] > > Create a project with the **Edge Functions** framework in the > [dashboard](https://app.lerix.dev) and copy its **API key** and **project id** > from **Project Settings**. ## 1. Install and initialize **Supabase** Store the keys as function secrets: ```bash theme={"dark"} supabase secrets set LERIX_KEY=... LERIX_PROJECT_ID=... ``` Import the package with an `npm:` specifier and wrap the handler you pass to `Deno.serve`: ```ts supabase/functions/send-welcome-email/index.ts theme={"dark"} import { Lerix } from 'npm:@lerix-dev/lerix-edge'; const lerix = Lerix.init({ apiKey: Deno.env.get('LERIX_KEY')!, projectId: Deno.env.get('LERIX_PROJECT_ID')!, app: { id: 'send-welcome-email' }, environment: 'production', }); Deno.serve(lerix.wrap(async (req) => { const { email } = await req.json(); // ... return Response.json({ ok: true }); })); ``` Reports are sent after the response through `EdgeRuntime.waitUntil`. **Cloudflare Workers** ```bash theme={"dark"} npm install @lerix-dev/lerix-edge npx wrangler secret put LERIX_KEY npx wrangler secret put LERIX_PROJECT_ID ``` Workers only expose secrets through the `env` argument, so pass a function to `Lerix.init`. `wrap()` calls it with `env` on the first request and sends reports through `ctx.waitUntil`: ```ts src/index.ts theme={"dark"} import { Lerix } from '@lerix-dev/lerix-edge'; const lerix = Lerix.init((env) => ({ apiKey: env.LERIX_KEY as string, projectId: env.LERIX_PROJECT_ID as string, app: { id: 'api-worker' }, })); export default { fetch: lerix.wrap(async (request: Request, env: Env, ctx: ExecutionContext) => { return new Response('ok'); }), }; ``` Turn on the `nodejs_compat` compatibility flag if you want manual reports and child spans to pick up the current request (it provides `AsyncLocalStorage`). Without it, errors thrown out of the handler are still reported with their request. **Vercel / Next.js** ```bash theme={"dark"} npm install @lerix-dev/lerix-edge @vercel/functions ``` ```ts app/api/checkout/route.ts theme={"dark"} import { Lerix } from '@lerix-dev/lerix-edge'; import { waitUntil } from '@vercel/functions'; export const runtime = 'edge'; const lerix = Lerix.init({ apiKey: process.env.LERIX_KEY!, projectId: process.env.LERIX_PROJECT_ID!, app: { id: 'checkout' }, }); export const POST = lerix.wrap(async (request) => { return Response.json({ ok: true }); }, { waitUntil }); ``` In `middleware.ts`, the `event` argument's `waitUntil` is used automatically. **Deno / Bun** ```ts theme={"dark"} import { Lerix } from 'npm:@lerix-dev/lerix-edge'; // Bun: '@lerix-dev/lerix-edge' const lerix = Lerix.init({ apiKey, projectId, app: { id: 'api' } }); Deno.serve(lerix.wrap(handler)); // Deno Bun.serve({ fetch: lerix.wrap(handler) }); // Bun ``` Long-running servers have no `waitUntil`: each response that produced a report waits for it, up to `flushTimeoutMs`. ## 2. Verify the integration Throw from the handler once: ```ts theme={"dark"} Deno.serve(lerix.wrap(() => { throw new Error('Lerix test error'); })); ``` The caller receives a `500` and the error appears in your [dashboard](https://app.lerix.dev) within seconds, with the request's method, URL and route. Remove the test afterwards. ## Options | Option | Default | Description | | - | - | - | | `apiKey` | required | Project API key | | `projectId` | required | Project id | | `app.id` | required | Stable name of the function, e.g. its folder name | | `app.name` / `app.version` / `app.buildNumber` | `id` / `0.0.0` / `0` | How the release is shown | | `environment` | | Sent as metadata with every report | | `url` | `https://api.lerix.dev/v1` | API base URL | | `enabled` | `true` | `false` turns reporting into a no-op, useful in tests | | `debug` | `false` | Log requests and reports | | `logger` | `console` | Where SDK messages go | | `installId` | `server:` | See below | | `ignoreErrors`, `beforeSend` | | Filter or edit reports; see [Error Tracking](/frameworks/edge/error-tracking) | | `shouldCapture` | all but 4xx | `(error, request) => boolean` for errors thrown out of `wrap()` | | `captureServerErrors` | `false` | Also report `5xx` responses the handler returns | | `captureUnhandled` | `true` | Report `error` / `unhandledrejection` events | | `exitOnUnhandled` | `true` | After reporting one, rethrow so the runtime ends the worker as it would have | | `flushTimeoutMs` | `2000` | Upper bound for `flush()` | | `tracing` | off | See [Request Tracing](/frameworks/edge/request-tracing) | | `notifications` | | `{ apiKey }` with a private key; see [Push Notifications](/frameworks/edge/push-notifications) | ## How your function appears in the dashboard The function registers as a `server` app named after `app.id`, once per runtime (`deno`, `workerd`, `edge-light`, `bun`). Reports show where they ran, for example `supabase:eu-central-1`. Edge isolates have no disk to keep an id in, so the SDK registers under a stable **install id** (`server:` by default). Every isolate and cold start of the same function and environment maps to the same user in the dashboard, instead of one new user per cold start. ## Next steps > **[Error Tracking](/frameworks/edge/error-tracking)** > > What `wrap()` reports, the error response, and manual reporting. > **[Request Tracing](/frameworks/edge/request-tracing)** > > A span per request, with `fetch` and Supabase database calls. > **[Push Notifications](/frameworks/edge/push-notifications)** > > Send push from a function, e.g. on a database webhook. # Edge Functions — Push Notifications Source: https://docs.lerix.dev/frameworks/edge/push-notifications Send, schedule, update and unsend push notifications from an edge function, for example on a Supabase database webhook. ## Introduction `lerix.notifications` is a typed client for the [push notifications REST API](/api-reference/push-notifications). A common setup is a Supabase **Database Webhook** that calls an edge function, which pushes to the users affected by the change. > [!WARNING] > > Sending needs a **private key** with the *send push notifications* > permission, created in the **Project Settings** of the project your app > uses. It is not the SDK key. Keep it in a > function secret, never in client code. ## Configure ```bash theme={"dark"} supabase secrets set LERIX_PUSH_KEY=... ``` ```ts theme={"dark"} const lerix = Lerix.init({ apiKey: Deno.env.get('LERIX_KEY')!, projectId: Deno.env.get('LERIX_PROJECT_ID')!, app: { id: 'order-status-push' }, notifications: { apiKey: Deno.env.get('LERIX_PUSH_KEY')! }, }); ``` `notifications.projectId` defaults to the SDK's `projectId`. Set it when your functions report to one project and your mobile app lives in another. ## Example: push when an order ships Create a Database Webhook on `UPDATE` of `orders` that calls this function: ```ts supabase/functions/order-status-push/index.ts theme={"dark"} Deno.serve(lerix.wrap(async (req) => { const { record, old_record } = await req.json(); if (record.status === old_record.status || record.status !== 'shipped') { return new Response('ignored'); } await lerix.notifications.send({ title: 'Your order is on its way', body: `Order #${record.number} has shipped.`, externalUserIds: [record.user_id], metadata: { orderId: record.id }, }); return new Response('sent'); })); ``` `externalUserIds` are your own user ids, as passed to `setUser()` in the client SDKs. One send reaches every device the user is signed in on, on every platform. See [Users & Identity](/modules/users-identity). ## Send | Field | Description | | - | - | | `title`, `body` | Required | | `externalUserIds` | Up to 1000 of your user ids; can't be combined with the other targets | | `deviceTokens` | Lerix device ids | | `sendByTopic` + `topic` | Broadcast to a topic's subscribers | | `sound`, `imageUrl`, `metadata` | Optional content | | `sentAt` | ISO string or `Date` to schedule for later | The result has the `notificationId`. For `externalUserIds` sends it also has `matchedUsers`, `matchedDevices` and `unknownUserIds` (users with no device yet, which is not an error). ## Update, unsend, cancel, deliveries ```ts theme={"dark"} await lerix.notifications.update(id, { title: 'Delivered', body: 'Enjoy!' }); await lerix.notifications.unsend(id); // remove from devices, keep history await lerix.notifications.cancel(id); // stop a scheduled one const rows = await lerix.notifications.deliveries(id); // per-recipient status ``` API errors throw `LerixApiException` with the backend's `code` and `status`. ## Identity verification When the project requires [identity verification](/modules/users-identity), each `setUser()` call needs an `identityHash` computed on your server. An edge function is a good place for it: ```ts theme={"dark"} import { identityHash } from 'npm:@lerix-dev/lerix-edge'; const hash = await identityHash(Deno.env.get('LERIX_IDENTITY_SECRET')!, user.id); return Response.json({ identityHash: hash }); ``` It is the lowercase hex HMAC-SHA256 of the user id, the same value the NestJS, Python and .NET SDKs compute. It is async here because Web Crypto is. # Edge Functions — Request Tracing Source: https://docs.lerix.dev/frameworks/edge/request-tracing One span per request, outbound fetch and Supabase database spans, sampling, and the trace behind every error — for edge functions. ## Introduction After the [installation](/frameworks/edge/installation), turn on request tracing to see each invocation of your function under **Requests** in the dashboard, with its route, duration, database calls and outbound requests. You can also open the exact request behind any error. See [Request Tracing](/modules/request-tracing) for what the dashboard shows. ## Enable it ```ts theme={"dark"} const lerix = Lerix.init({ apiKey: Deno.env.get('LERIX_KEY')!, projectId: Deno.env.get('LERIX_PROJECT_ID')!, app: { id: 'orders' }, tracing: { enabled: true }, }); Deno.serve(lerix.wrap(handler)); ``` Tracing is off by default. When it is off, `fetch` is not patched and nothing is queued. ## What is recorded * **One server span per wrapped request**, named `METHOD /route`. The route is the URL path with value-like segments replaced (`GET /orders/:id`). An incoming W3C `traceparent` header is continued, so a request from your app (or from another function) shows up as one trace. * **Outbound `fetch`**: every call inside the request becomes a `client` span named `METHOD host` and carries a `traceparent` header. Calls to the Lerix API itself are ignored. * **Supabase database calls**: requests that supabase-js sends to PostgREST are named like database spans: `select orders`, `insert orders`, `upsert orders`, `update orders`, `delete orders`, `rpc apply_discount`. Filters are in the query string and are never recorded. Turn the `fetch` hook off with `tracing: { enabled: true, instrumentations: { fetch: false } }`. Trace anything else by hand: ```ts theme={"dark"} const pdf = await lerix.trace('render invoice', () => renderInvoice(order)); ``` `lerix.trace()` adds a child span of the current request; outside a traced request it just runs the function. ## Sampling | Option | Default | Description | | - | - | - | | `sampleRate` | `0.1` | Share of successful requests whose full trace is kept | | `slowThresholdMs` | `2000` | Slower requests are always kept in full | | `maxSpansPerRequest` | `1000` | Child spans beyond this are dropped | | `flushIntervalMs` | `10000` | How long unsampled root spans may wait to be sent with a later request | The sampling decision is made once when a trace starts and travels downstream in `traceparent`. Requests that fail, run slow, or report an error are always kept in full. The root span of **every** request is sent, so the per-route counts, error rates and percentiles cover all traffic. Root spans of unsampled requests only feed those counters, so they are held in the isolate and sent together with a later request instead of one export per invocation. Anything sampled or failed is sent right after its response. ## The trace behind an error An error reported inside a traced request carries its trace id. Its page in the dashboard links to the request's waterfall, and that trace is kept even if sampling had dropped it. ## Client address The client address is recorded according to the project's tracing settings (masked by default). `Deno.serve` and Cloudflare (`cf-connecting-ip`) supply the caller's address directly. Supabase and Vercel only pass it in a forwarded header: set that header and the trusted proxy depth in the project settings, or no address is recorded. ## Limits * Child spans need `AsyncLocalStorage` to find their request. On Cloudflare, turn on `nodejs_compat`. On Vercel Edge, child spans depend on the runtime exposing a global `AsyncLocalStorage`. Without it you still get the server span for every request, and errors still link to it. * Cloudflare Workers only advance clocks across I/O, so pure-CPU work shows as zero duration there. # Flutter — Error Tracking Source: https://docs.lerix.dev/frameworks/flutter/error-tracking Report errors and exceptions from your Flutter app to the Lerix dashboard. ## Introduction Lerix captures errors and exceptions from your Flutter app so you can debug issues faster. After you complete the [Flutter installation](/frameworks/flutter/installation), use `Lerix.throwError` to send custom errors or caught exceptions to your dashboard. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.init()` before reporting errors. ## Report an error Use `Lerix.throwError` to manually report an error. This is useful when you catch an exception in a `try/catch` block and want to send it to Lerix with extra context. ```dart main.dart theme={"dark"} try { await riskyOperation(); } catch (e, stack) { await Lerix.throwError( e, stack, bugSeverity: BugSeverity.high, metaData: { 'userId': '123', 'action': 'payment', 'amount': 100.0, }, ); } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `error` | `Object` | Yes | Error object or message | | `stack` | `StackTrace` | Yes | Stack trace associated with the error | | `bugType` | `BugType` | No | Bug classification | | `bugSeverity` | `BugSeverity` | No | Severity level | | `metaData` | `Map` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `runtimeError` | An unhandled exception thrown at runtime | | `logicBug` | Incorrect behavior that doesn't throw | | `uiBug` | A visual or layout defect | | `networkError` | A failed or malformed network request | | `performance` | Slowness, jank, or excessive resource use | | `compatibility` | Breaks on a specific device, OS version, or platform | | `validationError` | Invalid input passed validation it shouldn't have | | `security` | A security-relevant defect | | `crash` | The app terminated unexpectedly | | `unknown` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `critical` | Blocks core functionality or affects all users | | `high` | Serious impact, but a workaround exists | | `medium` | Noticeable but limited impact | | `low` | Minor or cosmetic | | `unknown` | Severity not yet assessed | ### Automatic error capture Once `Lerix.init()` has run, uncaught Flutter framework errors (via `FlutterError.onError`) and uncaught Dart errors (via `PlatformDispatcher.instance.onError`) are captured and sent automatically — you don't need to wrap your whole app in a `try`/`catch` to get baseline coverage. `Lerix.throwError` is for cases you catch yourself and want to add classification or metadata to. Uncaught errors are reported as critical crashes on every platform, including Flutter Web. > [!TIP] > > Flutter Web release builds are minified. To see Dart file and line numbers for web errors, upload your source maps as described in [Flutter source maps](/frameworks/flutter/source-maps). Android and iOS release builds report them without any setup. ### Verify the integration Throw a test error to confirm everything is working: ```dart main.dart theme={"dark"} await Lerix.throwError( Exception('Test error'), StackTrace.current, ); ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. ## Filtering errors Requires `lerix_flutter` 0.3.0 or later. Two optional `Lerix.init` options decide which errors are sent. They apply to every report: uncaught Flutter, Dart and zone errors, `Lerix.throwError`, and web. ```dart theme={"dark"} Lerix.init( // ... ignoreErrors: [ 'SocketException', // case-insensitive substring RegExp(r'^HttpException: 40[13]'), // RegExp, matched with hasMatch ], beforeSend: (event) { if (event.metadata?['test'] == true) return null; // drop it return event.copyWith( message: event.message.replaceAll(RegExp(r'token=\S+'), 'token=***'), ); }, builder: () => runApp(const MyApp()), ); ``` * `ignoreErrors` (`List`) runs first. A `String` drops any error whose message contains it, ignoring case. Empty or whitespace-only strings are skipped. A `RegExp` drops any error whose message it matches. * `beforeSend` then gets a `LerixErrorEvent` (`message`, `stack`, `type`, `severity`, `metadata`). Return it to send it, return a changed copy from `copyWith`, or return `null` to drop it. If `beforeSend` throws, the original report is sent and the exception is never rethrown into your app. * A dropped report is not sent and doesn't call `onError`. * The message you match against is the issue title, which includes the error class. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # Flutter — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/flutter/feedback-analytics Add in-app feedback with annotated screenshots, an NPS survey and product analytics to your Flutter app. Requires `lerix_flutter` 0.4.0 or later. Everything on this page works on iOS, Android, macOS, Windows and the web. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). ## Setup Two additions to the app you set up in [Installation](/frameworks/flutter/installation): 1. Put `LerixFeedback` **above** your `MaterialApp`. It hosts the feedback flow and the NPS sheet, and costs nothing while they're closed. 2. Add `LerixNavigatorObserver` to your navigator so screen views are recorded. ```dart main.dart theme={"dark"} import 'package:flutter/material.dart'; import 'package:lerix_flutter/lerix_flutter.dart'; void main() { Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', builder: () => runApp(const LerixFeedback(child: MyApp())), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( navigatorObservers: [LerixNavigatorObserver()], home: const HomeScreen(), ); } } ``` With go\_router, pass the observer to `GoRouter` instead: ```dart theme={"dark"} final router = GoRouter( observers: [LerixNavigatorObserver()], routes: [/* ... */], ); ``` ### Init options `Lerix.init` takes two optional parameters for these modules: | Parameter | Type | Description | | - | - | - | | `environment` | `String?` | Sent with feedback, survey responses and events. Defaults to `dev` for debug builds, simulators, emulators and local web hosts, otherwise `prod`. | | `analytics` | `LerixAnalyticsOptions` | `enabled`, `autoSessions` and `autoScreens`, all `true` by default. | ```dart theme={"dark"} Lerix.init( // ... environment: 'staging', analytics: const LerixAnalyticsOptions(autoScreens: false), builder: () => runApp(const LerixFeedback(child: MyApp())), ); ``` ## In-app feedback Open the flow from anywhere below `LerixFeedback`, for example from a "Send feedback" button: ```dart theme={"dark"} final result = await Lerix.feedback.show(context); if (result.submitted) { print('Thanks! ${result.id ?? 'queued'}'); } ``` The app shrinks into a backdrop and keeps its state. The user writes a message, picks labels, captures screenshots by navigating the app and tapping **Capture**, draws on them (4 colours, 3 sizes, undo and redo), adds an email and reviews **Show details**. The screenshot step is hidden when the renderer can't capture the screen. `show()` returns a `LerixFeedbackResult`: | Field | Description | | - | - | | `submitted` | `true` when the user sent it (delivered, or safely queued) | | `id` | The server ID, when it was delivered before the call returned | | `clientId` | The ID the feedback is stored and deduplicated under | | `queued` | `true` when it is saved on the device and waiting to be sent | ### Options Labels, the email mode and the screenshot mode come from the dashboard. Override them for one call with `LerixFeedbackOptions`: ```dart theme={"dark"} await Lerix.feedback.show( context, const LerixFeedbackOptions( labels: ['bug', 'improvement'], // only these keys, in this order; [] hides the step email: LerixEmailMode.required, // hidden, optional, required screenshot: LerixScreenshotMode.optional, // hidden, optional prefillEmail: 'user@example.com', custom: {'plan': 'pro'}, // up to 50 keys / 16 KB ), ); ``` To prefill the email for every flow, call `Lerix.feedback.setUserEmail('user@example.com')` once the user signs in. ### Hide sensitive widgets Wrap anything that must not appear in a screenshot in `LerixConfidential`. It is painted over before the image is created. ```dart theme={"dark"} LerixConfidential( child: Text(cardNumber), ) ``` ### Your own UI Send feedback from your own form with `submit`. Screenshots are PNG, JPEG or WebP bytes, up to 5 MB each. ```dart theme={"dark"} final result = await Lerix.feedback.submit( message: 'Checkout button does nothing', labels: ['bug'], email: 'user@example.com', screenshots: [pngBytes], custom: {'plan': 'pro'}, ); ``` ### Look and language The flow follows your app's theme (light or dark, primary colour). English and Arabic (right to left) are built in, and other languages fall back to English. ```dart theme={"dark"} LerixFeedback( theme: LerixThemeData(/* ... */), // override the look strings: { 'fr': LerixStrings.en.copyWith(send: 'Envoyer'), }, child: const MyApp(), ) ``` ## NPS survey Call `maybeShowNps` at a calm moment, for example when the home screen appears. It returns `true` when the survey was shown. ```dart theme={"dark"} await Lerix.surveys.maybeShowNps(context); // While testing: skip the eligibility rules await Lerix.surveys.maybeShowNps(context, force: true); ``` The SDK only shows the survey when this install is due, using the rules you set in the dashboard (see [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown)). With `debugMode: true`, the console says why a survey was skipped. ## Analytics ```dart theme={"dark"} Lerix.track('checkout_started', {'items': 3, 'coupon': 'EID'}); Lerix.screen('Checkout'); // a screen the observer can't see, such as a tab ``` * `$first_open`, `$session_start` / `$session_end`, `$app_update` and `$screen_view` are recorded automatically. * Screen names come from the route's name: `GoRoute.name`, or the `name` in `RouteSettings` / `MaterialPage`. Unnamed routes, dialogs and bottom sheets are skipped. `LerixNavigatorObserver(includePopups: true)` also reports named popups, and `nameExtractor` lets you name routes yourself. * `Lerix.track` never throws and can be called before `Lerix.init`. Invalid names and properties are dropped and logged in debug mode. * Events are stored on the device and sent in batches. ```dart theme={"dark"} await Lerix.analytics.setEnabled(false); // opt out: persisted, clears the queue await Lerix.analytics.flush(); // send now ``` > [!NOTE] > > Call [`Lerix.setUser`](/modules/users-identity) after sign-in. Analytics > then counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs The last 50 breadcrumbs are attached to every feedback report. Screen views are added automatically. Add your own: ```dart theme={"dark"} Lerix.addBreadcrumb( 'Tapped pay', category: LerixBreadcrumbCategory.ui, // navigation, log, http, ui, custom data: {'amount': 49}, ); ``` # Flutter — Installation Source: https://docs.lerix.dev/frameworks/flutter/installation Add Lerix to your Flutter app in a few steps. ## Requirements * Flutter `>=3.0.0` * Dart `>=3.0.0` ### iOS requirements * Device with iOS 12+, iPadOS 12+, or Xcode simulator running iOS 16.2+ * Swift Package Manager (requires opt-in) or CocoaPods 1.16.2+ ### Android requirements * Android 7.0+ device or emulator with Google Play Store (Services) installed ## 1. Add the dependency Add `lerix_flutter` to your `pubspec.yaml`, plus the optional packages for the features you use: ```yaml pubspec.yaml theme={"dark"} dependencies: lerix_flutter: ^0.7.0 lerix_messaging: ^0.7.0 # push notifications lerix_app_update: ^0.7.0 # Google Play in-app updates (Android) ``` | Package | Add it for | | - | - | | `lerix_flutter` | Crash reporting, users, analytics, feedback, NPS and rating surveys, release control | | `lerix_messaging` | [Push notifications](/frameworks/flutter/push-notifications) | | `lerix_app_update` | [Play in-app updates](/frameworks/flutter/release-control) for required updates | Keep all three on the same version. The optional packages register themselves before `main()` runs: there is no extra Dart code to add. An app without `lerix_messaging` ships without Firebase, the notification permission or the Windows App SDK. Then run: ```bash theme={"dark"} flutter pub get ``` ## 2. Initialize the SDK Call `Lerix.init()` as early as possible — ideally at the top of `main()` in `main.dart`. Initialization sets up error monitoring and reporting. It does **not** initialize push notifications; call `Lerix.notifications.init()` separately when you need that module. ```dart main.dart theme={"dark"} import 'package:lerix_flutter/lerix_flutter.dart'; void main() { Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', builder: () => runApp(const MyApp()), ); } ``` > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `url` | `String` | Yes | The Lerix API URL for your project: `https://api.lerix.dev/v1/` followed by your project ID | | `apiKey` | `String` | Yes | Your project API key from the dashboard | | `projectId` | `String` | Yes | Your project ID from the dashboard | | `builder` | `Function` | Yes | Function that calls `runApp()` to start your app | | `debugMode` | `bool` | No | Enable debug logging (default: `false`) | | `onError` | `Function` | No | Custom error handler called when an error is captured | ### Async initialization If your `builder` needs async setup before `runApp()`, call `WidgetsFlutterBinding.ensureInitialized()` first: ```dart main.dart theme={"dark"} void main() { Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', builder: () async { WidgetsFlutterBinding.ensureInitialized(); await loadConfig(); runApp(const MyApp()); }, ); } ``` ### Debug mode Set `debugMode: true` during development to print SDK logs to the console: ```dart main.dart theme={"dark"} Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', debugMode: true, builder: () => runApp(const MyApp()), ); ``` ### Custom error handler Use `onError` to run your own logic when the SDK captures an error — for example, showing a fallback UI or logging to a secondary service: ```dart main.dart theme={"dark"} Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', builder: () => runApp(const MyApp()), onError: (error) { print('Custom error handler: ${error.message}'); }, ); ``` ## 3. Verify the integration Throw a test error to confirm everything is working: ```dart main.dart theme={"dark"} await Lerix.throwError( Exception('Test error'), StackTrace.current, ); ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/flutter/error-tracking) to report custom errors, or [Push Notifications](/frameworks/flutter/push-notifications) to enable push delivery. # Flutter — Push Notifications Source: https://docs.lerix.dev/frameworks/flutter/push-notifications Configure push notifications for Android, iOS, Web, and Huawei on Flutter — one package, one API, for every platform. ## Introduction Lerix delivers push notifications using each platform's native service — Firebase Cloud Messaging (FCM) on Android, Apple Push Notification service (APNs) directly on iOS (no Firebase required), and standard Web Push (VAPID) on Flutter Web (no Firebase or OneSignal required there either). One package, one `Lerix.notifications.*` API, for all three. After you complete the [Flutter installation](/frameworks/flutter/installation), follow the platform steps below to enable push delivery in your app. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.init()` before configuring push notifications. ## Add the package Push lives in its own package, `lerix_messaging`, next to `lerix_flutter` and on the same version: ```yaml pubspec.yaml theme={"dark"} dependencies: lerix_flutter: ^0.7.0 lerix_messaging: ^0.7.0 ``` That's all on the Dart side: the package registers itself before `main()` runs, and `Lerix.notifications` works as described below. Without it, `Lerix.notifications.init()` throws `ERR_MESSAGING_NOT_INSTALLED`. > [!NOTE] > > Coming from `lerix_flutter` 0.4? Add `lerix_messaging`, and in `AppDelegate.swift` (iOS and macOS) change `import lerix_messaging` to `import lerix_messaging`. The service worker and `NotificationService.swift` now come from `lerix_messaging`; the files themselves didn't change. ## Android configuration No additional SDK setup is required for Android. You only need to add your Firebase `google-services.json` file to the project. 1. Create or open your app in the [Firebase Console](https://console.firebase.google.com/). 2. Download `google-services.json` for your Android app. 3. Upload the file to the Lerix dashboard. For the full setup steps, see [Firebase's Android client setup guide](https://firebase.google.com/docs/cloud-messaging/android/client). ## iOS configuration You need an active [Apple Developer account](https://developer.apple.com/programs/) to send push notifications on iOS. ### Set up APNs authentication 1. In the [Apple Developer portal](https://developer.apple.com/account/resources/authkeys/list), go to **Certificates, Identifiers & Profiles → Keys**. 2. Create a new key with **Apple Push Notifications service (APNs)** enabled. 3. Download the `.p8` authentication key and note the **Key ID**. You can only download the key once. 4. Upload the `.p8` file to the Lerix dashboard. For the full setup steps, see [Apple's guide to registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns). ### AppDelegate configuration Add the following overrides to `ios/Runner/AppDelegate.swift`: ```swift AppDelegate.swift theme={"dark"} import lerix override func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceData: Data ) { lerix_didRegisterForRemoteNotifications(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceData) } override func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { lerix_didFailToRegisterForRemoteNotifications(application, didFailToRegisterForRemoteNotificationsWithError: error) } ``` ## macOS configuration macOS uses APNs with the same key you uploaded for iOS; no extra dashboard setup is needed. In Xcode, open `macos/Runner`: 1. **Signing & Capabilities:** sign with your Apple team and add **Push Notifications**. An app signed "to run locally" can't receive pushes. 2. **App Sandbox:** allow **Outgoing Connections (Client)** so the SDK can reach Lerix. 3. **AppDelegate:** forward the APNs callbacks in `macos/Runner/AppDelegate.swift`: ```swift theme={"dark"} import lerix_messaging override func application(_ application: NSApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { lerix_didRegisterForRemoteNotifications(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } override func application(_ application: NSApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { lerix_didFailToRegisterForRemoteNotifications(application, didFailToRegisterForRemoteNotificationsWithError: error) } override func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String: Any]) { lerix_didReceiveRemoteNotification(application, didReceiveRemoteNotification: userInfo) } ``` > [!NOTE] > > For Windows, see [Windows push notifications](/frameworks/flutter/windows) (preview). Linux push isn't supported yet. ## Web configuration Flutter Web is supported by the same `lerix_flutter` package — no separate JS SDK, no Firebase project. Two steps instead of Xcode's: 1. **Copy the service worker.** A Flutter package can't add a file to your app's `web/` folder for you — copy the `lerix_messaging` package's `web_sw/lerix-sw.js` into your app's `web/lerix-sw.js`, so it's served at your site's root (e.g. `https://yoursite.com/lerix-sw.js`). 2. **Generate a Web Push key** from the dashboard: **Notifications → Settings → Web Push → Generate keys**. One click — Lerix generates and stores the VAPID key pair for you; there's nothing to copy into your code. > [!NOTE] > > Lerix registers its worker on its own scope (`/lerix/`), so it runs alongside Flutter's `flutter_service_worker.js` and a default `flutter build web` works as is. With `lerix_flutter` 0.0.1, the two workers competed for the root scope and Flutter's replaced Lerix's, so pushes were reported as sent but never shown. If you are still on 0.0.1, upgrade, or build with `flutter build web --pwa-strategy=none`. > [!NOTE] > > On web, `requestPermissions()` does more than it does on mobile — see [Platform differences](#platform-differences-on-web) below. ## Huawei configuration Coming Soon Huawei push notification support is not available yet. Check back soon for setup instructions. ## Dashboard settings Once you've uploaded your `google-services.json` (Android) and/or `.p8` key (iOS), or generated a Web Push key pair, from the project's **Notifications → Settings** tab in the dashboard, that platform card shows a **Configured** badge and the credentials it holds — click it again any time to review them or replace them. ## Flutter code All notification methods live on `Lerix.notifications`, and require `await Lerix.notifications.init()` to have run first — registration with APNs/FCM happens automatically as part of `init()`, so there's no separate "register" call for the normal flow. ```dart main.dart theme={"dark"} await Lerix.notifications.init(); final granted = await Lerix.notifications.requestPermissions(); Lerix.notifications.setOnNotificationReceived((payload) { // Fired while the app is open (foreground) — show an in-app banner, etc. debugPrint("Notification received: ${payload.title} — ${payload.body}"); }); Lerix.notifications.setOnNotificationTapped((payload) { // Fired when the user taps the notification — including one that // launched the app from a fully closed state (see the note below). debugPrint("Notification tapped: ${payload.metadata}"); }); final deviceId = await Lerix.notifications.getDeviceId(); final status = await Lerix.notifications.checkPermissionStatus(); // "authorized", "denied", "notDetermined" ``` Register `setOnNotificationTapped` somewhere that has access to navigation (e.g. a global `navigatorKey`), since a tap almost always means routing to a specific screen based on `payload.metadata` rather than just logging it. ## The notification payload `setOnNotificationReceived` and `setOnNotificationTapped` both receive an `LerixNotificationPayload` — the same shape on iOS and Android, regardless of how differently each platform actually delivers the underlying push: ```dart theme={"dark"} class LerixNotificationPayload { String? notificationId; // this notification's id in Lerix — the same id used for delivery status and unsend String? title; String? body; Map metadata; // whatever you passed as `metadata` when sending String? imageUrl; // whatever you passed as `imageUrl` when sending, if any } ``` ```dart theme={"dark"} Lerix.notifications.setOnNotificationTapped((payload) { final screen = payload.metadata['screen']; final orderId = payload.metadata['orderId']; // Navigate based on payload.metadata, etc. }); ``` > [!NOTE] > > A tap is delivered reliably even when it's what launched the app from a > fully terminated state — call `setOnNotificationTapped` right after > `Lerix.notifications.init()` and any tap that happened before your app > finished starting up is replayed automatically once the callback is > registered. ## Device identifiers: `getDeviceId()` vs `getDeviceToken()` These look similar but answer different questions — use the right one for what you're doing: | Method | Returns | Use it for | | - | - | - | | `getDeviceId()` | Lerix's own id for this device, assigned the first time `init()` registers it with the backend | The value to put in `deviceTokens` when sending a notification to this specific device — from the dashboard, or the [REST API](/api-reference/push-notifications) | | `getDeviceToken()` | The raw platform push token — the APNs device token on iOS, the FCM registration token on Android. Always `null` on web (the equivalent there is a full `PushSubscription` object, not a meaningful single "token" to expose) | Rarely needed directly; useful for diagnostics or if you're calling APNs/FCM yourself outside of Lerix | ```dart theme={"dark"} final deviceId = await Lerix.notifications.getDeviceId(); print('Send test notifications to: $deviceId'); final token = await Lerix.notifications.getDeviceToken(); print('Raw platform token: $token'); ``` Both return `null` until registration completes, which `Lerix.notifications.init()` handles automatically — no separate call needed. ## Sending a custom sound Set `sound` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`sound` parameter](/api-reference/push-notifications) — to play a bundled sound file instead of the device default: * The file must already ship inside your app project — Lerix only passes the filename through, it never hosts or uploads sound files. * **iOS**: pass the filename with its extension, e.g. `sound.mp3` (`.wav`/`.caf` also supported). * **Android**: pass the filename **without** an extension, matching a file under `res/raw/` (e.g. `sound` for `res/raw/sound.mp3`). Supported formats: `.wav`, `.mp3`, `.ogg`. * **Web**: not supported — the Web Notifications API has no cross-browser way to set a custom sound, so this parameter is ignored for web recipients. > [!NOTE] > > On Android, a sound is locked in the first time it's used on a given > device — the OS fixes a notification channel's sound at creation and never > lets it change afterward. Sending a different `sound` value later creates a > new channel rather than updating the old one; that's expected Android > behavior, not a bug. No SDK code is required to play the sound — it happens automatically as part of the system notification. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the device inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```dart theme={"dark"} Lerix.notifications.setOnNotificationTapped((payload) { final chatId = payload.metadata['chatId']; // Navigate to the chat screen, etc. }); ``` ## Sending an image Set `imageUrl` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications) — to show a rich image in the notification. Unlike `sound`, this is a normal `https://` URL, not a bundled file — Lerix downloads it on your behalf. * **Android**: works automatically, no extra setup. The image renders as a `BigPictureStyle` notification. * **Web**: works automatically, no extra setup — the service worker renders it as the notification's icon/image directly. * **iOS**: requires your app to have a **Notification Service Extension** — APNs has no native "image" field, so the OS needs a small extension to download and attach the image before displaying the notification. This is a one-time setup step per app: 1. In Xcode: **File → New → Target… → Notification Service Extension**. Name it (e.g. `NotificationServiceExtension`). 2. Replace the generated `NotificationService.swift` with the one from the `lerix_messaging` package at `ios/NotificationServiceExtension/NotificationService.swift`. 3. Build and run — no other configuration needed. The backend automatically sets APNs' `mutable-content` flag whenever a notification carries an `imageUrl`, which is what triggers your extension to run. > [!NOTE] > > If iOS's extension isn't set up, an `imageUrl` is silently ignored on iOS — > the notification still shows normally with no image, it doesn't fail to send. Android is unaffected either way. The image is also available in Dart as `payload.imageUrl` (in `setOnNotificationReceived`/`setOnNotificationTapped`) if you want to show it in a custom in-app banner, in addition to the OS rendering it natively. ## Platform differences on web Flutter Web uses the same `Lerix.notifications.*` calls as mobile, with a few real differences worth knowing about rather than being surprised by: * **`requestPermissions()` does more on web.** On mobile, push registration happens automatically inside `init()`, and `requestPermissions()` only concerns the visible-notification permission. A browser can't subscribe to push before permission is granted, so on web, `requestPermissions()` *also* registers the service worker's push subscription with the backend once the user allows it — the same method call, just doing double duty. * **`getSenderId()` returns `null` on web** — it's an Android/FCM-only concept (Web Push uses VAPID, not Firebase). * **`getDeviceToken()` also returns `null` on web** — the browser's `PushSubscription` isn't a single opaque token the way APNs/FCM's are, so there's nothing meaningful to return. Use `getDeviceId()` on every platform instead. * **Custom sound isn't supported** — see [Sending a custom sound](#sending-a-custom-sound) above. * **Browser support**: Chrome, Firefox, and Edge fully; Safari requires macOS 13+ or iOS 16.4+ — older Safari doesn't support standard Web Push at all. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```dart theme={"dark"} await Lerix.setUser('user_123'); // after login await Lerix.clearUser(); // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```dart theme={"dark"} final subscribed = await Lerix.notifications.subscribeToTopic('promotions'); await Lerix.notifications.unsubscribeFromTopic('promotions'); ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. # Release control in Flutter Source: https://docs.lerix.dev/frameworks/flutter/release-control Show update prompts, required updates and maintenance screens in your Flutter app, or build your own. Release control works on iOS, Android, macOS, Windows and the web. For what it does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default once `Lerix.init` runs, with the built-in screens. There is nothing to add for the default behaviour. To change it, pass `LerixAppControlOptions`: ```dart main.dart theme={"dark"} Lerix.init( url: 'https://api.lerix.dev/v1/YOUR_PROJECT_ID', apiKey: 'your-api-key', projectId: 'your-project-id', appControl: const LerixAppControlOptions( enabled: true, // false: no checks, no screens, no events ui: true, // false: draw your own screens usePlayInAppUpdate: true, // Android + lerix_app_update: Google Play's in-app update for required updates ), builder: () => runApp(const MyApp()), ); ``` | Option | Default | Description | | - | - | - | | `enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | | `usePlayInAppUpdate` | `true` | On Android, with the `lerix_app_update` package added: when the app was installed from Google Play and Play offers the update, a required update starts Play's full-screen in-app update instead of opening the store page, and resumes it if the user leaves. Without the package it does nothing, and the app never ships Play's update library. | ### Version The SDK sends the version and build number from your `pubspec.yaml` (`version: 3.4.0+412` sends `3.4.0` and build `412`). On the web it sends the platform `web`, and the version only when the build has a `version.json` (`flutter build web` writes one). Rules compare build numbers when both sides have one. ### Instant changes with silent push When you start or end a maintenance window or force an update, Lerix sends a silent push so apps in the background refresh too. It reaches apps that call `Lerix.notifications.init()`, as in [Push Notifications](/frameworks/flutter/push-notifications), and the SDK handles it for you: it is never shown, and never passed to `setOnNotificationReceived`. On iOS and macOS it needs the `lerix_didReceiveRemoteNotification` hook in your `AppDelegate` from that guide, and the **Remote notifications** background mode. Without push, and on the web, apps pick up the change at their next check. ## Built-in screens There is no widget to add. The SDK inserts its screens into your app's top-level `Overlay`, above every route your navigator pushes. * **Maintenance**: covers the whole app, with a **Try again** button. Back on Android sends the app to the background. * **Required update**: the same full-screen host, with release notes under "What's new" and an **Update** button that opens the store (web: **Reload**). * **Update prompt**: a dialog (web: a bar at the bottom) with **Update**, **Later** and, when allowed, **Skip this version**. It appears at most once per app run. Back closes it without saving **Later**. The screens follow your app's theme and the device language (English, or Arabic right to left). If you use `LerixFeedback`, its `theme` and `locale` apply here too. Widgets that you place above the navigator in `MaterialApp.builder` are not covered. See [Custom UI](/modules/release-control/custom-ui#built-in-screens) for when each screen appears. ## Your own screens Set `ui: false`, then listen to `Lerix.appControl`, a `ValueListenable`. The state has `update` (`LerixUpdateInfo`), `maintenance` (`LerixMaintenanceInfo`) and `sdk` (`LerixSdkControl`), each `null` when nothing applies (fields in [The state](/modules/release-control/custom-ui#the-state)). ```dart release_gate.dart theme={"dark"} class ReleaseGate extends StatelessWidget { const ReleaseGate({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: Lerix.appControl, builder: (context, state, _) { final maintenance = state.maintenance; final update = state.update; if (maintenance != null) { return MaintenanceScreen( title: maintenance.title, message: maintenance.message, onRetry: () => Lerix.appControl.refresh(), ); } if (update != null && update.state == LerixUpdateState.required) { return ForceUpdateScreen( message: update.message, onUpdate: () => Lerix.appControl.openStore(), ); } return child; }, ); } } ``` `update.isRequired` is a shortcut for the same check. For a soft prompt, call the helpers from your own dialog: ```dart theme={"dark"} Lerix.appControl.reportPromptShown(update); // records $update_prompt_shown await Lerix.appControl.openStore(); // Update; returns whether something opened await Lerix.appControl.later(); // hide for remindAfterHours await Lerix.appControl.skip(); // hide until a newer version (optional updates only) ``` `Lerix.appControl.stream` emits every new state, if you prefer a stream. ## Check now ```dart theme={"dark"} final state = await Lerix.appControl.refresh(); ``` `refresh()` (or its alias `checkNow()`) ignores the saved answer's lifetime, fetches, and returns the new state. It never throws: offline, it returns the saved state. The SDK already checks on launch, when the app returns to the foreground, and every minute during an emergency (see [Delivery and offline behaviour](/modules/release-control/delivery-offline)). # Flutter — Source Maps Source: https://docs.lerix.dev/frameworks/flutter/source-maps Upload your Flutter Web source maps privately so Lerix shows web errors at the original Dart file and line. ## Introduction Flutter Web release builds are compiled to minified JavaScript, so a stack trace from a real user looks like this: ```text theme={"dark"} main.dart.js:64858 ``` When you upload your build's source maps to Lerix, the dashboard maps each frame back to your Dart code, for example `lib/chat/chat_store.dart:15`, with the Dart member name. The maps are uploaded privately and deleted from your build output before you deploy. They are never served publicly, so your source code stays private. > [!NOTE] > > Complete the [Flutter installation](/frameworks/flutter/installation) first. > Web stack traces require `lerix_flutter` 0.0.2 or newer. > [!NOTE] > > Source maps are only needed for Flutter Web. Android and iOS release builds > already report Dart file and line numbers, see > [Mobile builds](#mobile-builds). ## 1. Build with source maps Add `--source-maps` to your web build: ```bash theme={"dark"} flutter build web --source-maps ``` Check that `main.dart.js.map` appears next to `main.dart.js` in `build/web`. ## 2. Create a private API key Uploading source maps needs a private key, separate from the public project key your app uses. 1. Open your project in the [Lerix Dashboard](https://app.lerix.dev). 2. Go to **Project settings** → **API keys**. 3. Create a key with the **Upload source maps** permission (or **Full access**). > [!WARNING] > > This key is secret. Store it as a CI secret and never put it in client code > or commit it to your repository. ## 3. Upload the maps after each build After `flutter build web --source-maps` and before you deploy, run the Lerix CLI: ```bash theme={"dark"} npx @lerix-dev/lerix-cli upload ``` Run it from your Flutter project folder. It finds `build/web` on its own and uses the `version` from `pubspec.yaml`, without the `+build` number, as the release. For `version: 1.4.0+12`, the release is `1.4.0`. The CLI uploads the `.map` files, then deletes them from `build/web` so they are never deployed. If any upload fails, it keeps the maps and exits with code 1, so you can retry the step. The CLI requires Node.js 18.17 or newer. > [!WARNING] > > Upload after every build. `main.dart.js` keeps the same name across builds, > so the release is what tells builds apart. Bump `version` in `pubspec.yaml` > for each release. ### Options Each option can also be set with an environment variable. | Option | Environment variable | Description | | - | - | - | | `--release` | `LERIX_RELEASE` | The version this build ships as. Defaults to `version` in `pubspec.yaml`, without the `+build` number | | `--api-key` | `LERIX_API_KEY` | Your normal public project API key | | `--project-id` | `LERIX_PROJECT_ID` | Your project ID | | `--secret-key` | `LERIX_SECRET_KEY` | The private key from step 2 | | `--url` | `LERIX_URL` | Lerix API URL. Defaults to `https://api.lerix.dev/v1` | | `--keep-maps` | | Keep the `.map` files in the build output instead of deleting them | | `--dry-run` | | List the maps that would be uploaded, without uploading anything | | `--optional` | | Skip the upload instead of failing when the keys aren't set (local builds). The `.map` files are still deleted | ## 4. Nothing to pass to the SDK The Flutter SDK reports the same release on its own: the `version` from `pubspec.yaml`, without the `+build` number. You don't need to set a release in `Lerix.init()`. ## Example: GitHub Actions This workflow builds the web app, uploads the maps, then deploys. Add `LERIX_API_KEY`, `LERIX_PROJECT_ID` and `LERIX_SECRET_KEY` as repository secrets. ```yaml .github/workflows/deploy.yml theme={"dark"} jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - uses: actions/setup-node@v4 with: node-version: 20 - run: flutter pub get - run: flutter build web --source-maps - run: npx @lerix-dev/lerix-cli upload env: LERIX_API_KEY: ${{ secrets.LERIX_API_KEY }} LERIX_PROJECT_ID: ${{ secrets.LERIX_PROJECT_ID }} LERIX_SECRET_KEY: ${{ secrets.LERIX_SECRET_KEY }} # Deploy build/web here. It no longer contains .map files. ``` ## What you see in the dashboard New web errors show the original Dart file, line and member name. Flutter framework frames appear as `package:flutter/...` and are dimmed as library code. The AI assistant, GitHub issues and the MCP server all receive the readable stack too. * dart2js source maps don't embed your source code, so web errors show the file and line without a code snippet. * Only errors reported after the maps are uploaded are mapped. Earlier errors keep their minified stack. ## Mobile builds Android and iOS release builds already report Dart file and line numbers. No setup is needed. Builds made with `--obfuscate --split-debug-info` are not symbolicated yet. ## Troubleshooting | Problem | Fix | | - | - | | `no .js.map files found` | The build has no source maps. Run `flutter build web --source-maps` and run the CLI from your Flutter project folder. | | `Uploading source maps needs a private API key...` (`SOURCE_MAP_SECRET_KEY_MISSING`) | `--secret-key` is missing or invalid. Create a private key as described in step 2. | | `PROJECT_API_KEY_DONT_HAVE_PERMISSION` | The private key lacks the **Upload source maps** permission. Edit the key or create a new one with that permission. | | The stack is still minified | Check that you uploaded the maps from this exact build, that `version` in `pubspec.yaml` changed since the previous release, and that the error is new. | # Flutter: Windows push notifications Source: https://docs.lerix.dev/frameworks/flutter/windows Send push notifications to Flutter apps on Windows through WNS, using a Microsoft Entra ID app registration. > [!WARNING] > > Windows push is in **preview**. The Lerix side (dashboard, API and delivery) is ready; the Windows part of the Flutter SDK is still being tested. Expect changes before the stable release. ## How it works Lerix sends Windows notifications through the **Windows Push Notification Service (WNS)**, using the [Windows App SDK](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/) push APIs. These use a **Microsoft Entra ID** app registration as your app's identity, not a Microsoft Store (Partner Center) account. 1. When your app starts, the SDK asks Windows for a push **channel** and registers it with Lerix as the device's token. Channels expire after 30 days, so the SDK requests a fresh one on every launch. 2. When you send, Lerix gets an access token from Microsoft with your Entra ID credentials and sends the notification to WNS as a **toast**. 3. Windows displays the toast itself. When the user clicks it, your app receives the notification in `setOnNotificationTapped`. The Dart API is the same as on every other platform; there is no Windows-specific code in your app. ## Requirements * The `lerix_messaging` package next to `lerix_flutter` (see [Push notifications](/frameworks/flutter/push-notifications#add-the-package)). Only apps with it download the Windows App SDK at build time; `lerix_flutter` alone builds offline. * Windows 10 version 2004 or later, or Windows 11. * The **Windows App Runtime 1.7** on the user's machine. Bundle Microsoft's `WindowsAppRuntimeInstall-x64.exe` with your installer. * A Microsoft Entra ID app registration (free). * To receive notifications **while the app is closed**: package identity, through a sparse package (see [Delivery when the app is closed](#delivery-when-the-app-is-closed)). > [!NOTE] > > An app installed from a plain `.exe` without package identity can receive notifications **only while it's running**, including when it's minimized to the tray. ## Step 1: Create a Microsoft Entra ID app registration 1. In the [Azure portal](https://portal.azure.com), open **Microsoft Entra ID → App registrations → New registration**. 2. Under **Supported account types**, choose **Accounts in any organizational directory** (multi-tenant). Windows App SDK push requires it. 3. After creating it, copy the **Application (client) ID** and the **Directory (tenant) ID**. 4. Open **Certificates & secrets → New client secret** and copy the secret's **value** right away. It's shown only once. 5. Back on the **Overview** page, click the link next to **Managed application in local directory** and copy the **Object ID** shown there. Don't use the object ID on the registration's own overview page; it's a different one. ## Step 2: Add the credentials in Lerix In the dashboard, open your project and go to **Notifications → Settings → Windows Configuration**. Enter the tenant ID, client ID, client secret and object ID, then click **Save**. Lerix checks the credentials with Microsoft before saving them. If Microsoft rejects them, you see `WINDOWS_CREDENTIALS_INVALID` and nothing is saved. The client secret is stored encrypted and never shown again. > [!NOTE] > > Your project needs the **Flutter** platform for the Notifications screens to appear. Add it in **Settings → General → Platforms** if needed. ## Step 3: Initialize the SDK Nothing changes in your Dart code. Initialize Lerix and notifications as on other platforms: ```dart theme={"dark"} await Lerix.notifications.init(); ``` On Windows, `init()` creates the push channel and registers the device with Lerix. Windows has no permission prompt; `requestPermissions()` returns `false` only if the user turned notifications off for your app in Windows **Settings**. To reach the same person on Windows and their other devices, link the install to your user ID after login. See [Identify users](/modules/users-identity). ## Delivery when the app is closed For Windows to show your notifications while the app isn't running, the app needs **package identity**. You can keep your normal `.exe` installer and add a **sparse package** ("packaged with external location") that gives the app that identity. 1. Get a **code-signing certificate**. The sparse package must be signed. 2. Create and register the sparse package from your installer. Microsoft's guide: [Grant package identity by packaging with external location](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps). 3. Ask Microsoft to map your package to your Entra ID app. Email `Win_App_SDK_Push@microsoft.com` with the subject **Windows App SDK Push Notifications Mapping Request** and include your **Package Family Name**, **Application (client) ID** and **Object ID**. > [!WARNING] > > Microsoft processes mapping requests **once a week**. Until the mapping is done, sends to packaged installs fail with `WNS_FORBIDDEN`. Plan for this lead time before your launch. ## What works on Windows | Feature | Windows | | - | - | | Send to devices, topics and [users](/modules/users-identity) | ✅ | | Title, body, metadata | ✅ | | Images | ✅ HTTPS URLs only | | `setOnNotificationTapped`, including a click that launches the app | ✅ | | `setOnNotificationReceived` while the app is open | ❌ Windows shows the toast itself | | Unsend (remove a delivered notification) | ❌ WNS can't remove a toast from a Windows device | | Custom sounds | ❌ | A Windows notification can't be larger than 5000 bytes, including metadata. Larger sends fail with `PAYLOAD_TOO_LARGE`. ## Troubleshooting These codes appear in a notification's [delivery report](/api-reference/notification-deliveries): | Code | Meaning | | - | - | | `PLATFORM_NOT_CONFIGURED` | No Windows credentials in this project yet. Complete step 2. | | `WINDOWS_CREDENTIALS_INVALID` | Microsoft no longer accepts the saved credentials, for example an expired client secret. Create a new secret and save it again. | | `WNS_FORBIDDEN` | The credentials are valid but WNS won't accept them for this device. Check that the **Object ID** is the one under **Managed application in local directory** of the **same** app registration as the client ID, and that the registration is **multi-tenant**. For packaged installs, also check that the package is mapped to your Entra ID app. | | `WNS_CHANNEL_GONE` | The device's channel expired or the app was uninstalled. Lerix deletes the device; it registers again on the next launch. | | `WNS_THROTTLED` | WNS is rate-limiting your sends. Retry later. | | `PAYLOAD_TOO_LARGE` | Over the 5000-byte limit. Shorten the body or metadata. | # iOS (Swift) — Error Tracking Source: https://docs.lerix.dev/frameworks/ios/error-tracking Report errors and automatically capture crashes from your native iOS app to the Lerix dashboard. ## Introduction Lerix captures errors and crashes from your native iOS app so you can debug issues faster. After you complete the [iOS installation](/frameworks/ios/installation), use `Lerix.throwError` to send custom errors or caught exceptions to your dashboard — uncaught crashes are captured automatically, with no extra code required. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.initialize()` before reporting errors. ## Report an error Use `Lerix.throwError` to manually report an error. This is useful when you catch an error yourself and want to send it to Lerix with extra context. ```swift theme={"dark"} do { try riskyOperation() } catch { Lerix.throwError( "\(error)", stack: Thread.callStackSymbols, type: .runtimeError, severity: .high, metadata: ["userId": "123", "action": "payment", "amount": 100] ) } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `issue` | `String` | Yes | Error message or description | | `stack` | `[String]` | No | Stack trace frames — pass `Thread.callStackSymbols` for the current call stack | | `type` | `BugType` | No | Bug classification (default: `.runtimeError`) | | `severity` | `BugSeverity` | No | Severity level (default: `.medium`) | | `metadata` | `[String: Any]?` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `runtimeError` | An unhandled exception thrown at runtime | | `logicBug` | Incorrect behavior that doesn't throw | | `uiBug` | A visual or layout defect | | `networkError` | A failed or malformed network request | | `performance` | Slowness, jank, or excessive resource use | | `compatibility` | Breaks on a specific device, OS version, or platform | | `validationError` | Invalid input passed validation it shouldn't have | | `security` | A security-relevant defect | | `crash` | The app terminated unexpectedly | | `unknown` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `critical` | Blocks core functionality or affects all users | | `high` | Serious impact, but a workaround exists | | `medium` | Noticeable but limited impact | | `low` | Minor or cosmetic | | `unknown` | Severity not yet assessed | ## Automatic crash capture `Lerix.initialize()` installs a crash handler by default, covering both: * **Uncaught exceptions** (`NSException` — e.g. an unimplemented selector, an invalid collection operation). * **Fatal signals** (`SIGABRT`, `SIGILL`, `SIGSEGV`, `SIGFPE`, `SIGBUS`, `SIGTRAP`) — this is what catches typical Swift runtime traps: force-unwrapping `nil`, an array index out of range, a failed `precondition`, and similar. A crash can't reliably make a network call — the process is already terminating — so the report is written to disk synchronously at crash time, then sent automatically the *next* time your app launches, tagged `type: .crash, severity: .critical`. You don't need to do anything to receive it; it shows up in the dashboard on the following launch. To disable it (e.g. if your app already has its own crash reporter and you only want manual `throwError` calls): ```swift theme={"dark"} Lerix.initialize( apiKey: "your-api-key", projectId: "your-project-id", enableCrashReporting: false ) ``` > [!NOTE] > > Stack traces are symbolicated using the app's own debug symbols at the time of the crash, so a Release build with symbols stripped may show addresses instead of function names for some frames — this is a normal characteristic of native crash reporting, not a bug in the SDK. ### Verify the integration Throw a test error to confirm everything is working: ```swift theme={"dark"} Lerix.throwError("Test error", stack: Thread.callStackSymbols) ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. ## Filtering errors Requires the iOS/macOS SDK 1.2.0 or later. Drop or rewrite error reports on the device before they are sent. Filtering runs on every report: manual `throwError` calls, and crash reports saved by the crash handler and sent on the next launch. ```swift theme={"dark"} Lerix.initialize( apiKey: "", projectId: "", // Dropped if any entry is a case-insensitive substring of the message. ignoreErrors: ["The network connection was lost"], // Dropped if any regex matches anywhere in the message. ignoreErrorPatterns: [try! NSRegularExpression(pattern: "^NSURLErrorDomain -10\\d{2}")], // Runs after the ignore lists, right before sending. Return the // (optionally modified) event to send it, or nil to drop it. beforeSend: { event in var event = event event.message = event.message.replacingOccurrences(of: "secret", with: "[redacted]") if event.severity == .low { return nil } return event } ) ``` * `ignoreErrors` (`[String]`) and `ignoreErrorPatterns` (`[NSRegularExpression]`) run first. Blank strings are skipped. * `beforeSend` then gets a `LerixErrorEvent` with `message`, `stack`, `type`, `severity` and `metadata`, all mutable. Return it (changed or not) to send it, or `nil` to drop it. * The message you match against is the issue title, which includes the error class. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # iOS (Swift) — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/ios/feedback-analytics Add in-app feedback, shake to report, an NPS survey and product analytics to your native iOS or macOS app. Requires `Lerix` 1.3.0 or later. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). > [!NOTE] > > On macOS the feedback sheet has no screenshot step. Everything else on this > page works on every system the SDK supports (iOS 15+, macOS 12+). ## Setup Analytics starts when `Lerix.initialize` runs. Two optional parameters configure these modules: ```swift AppDelegate.swift theme={"dark"} Lerix.initialize( apiKey: "your-api-key", projectId: "your-project-id", environment: "staging", // default: "dev" for debug builds and the simulator, else "prod" analytics: LerixAnalyticsOptions( enabled: true, autoSessions: true, autoScreens: true, uikitScreenTracking: false // true: a $screen_view per UIViewController ) ) ``` | Parameter | Type | Description | | - | - | - | | `environment` | `String?` | Sent with feedback, survey responses and events | | `analytics` | `LerixAnalyticsOptions` | `enabled`, `autoSessions`, `autoScreens` (default `true`) and `uikitScreenTracking` (default `false`) | ## In-app feedback Call `show` from anywhere, in UIKit or SwiftUI. The current screen is captured first, then the sheet opens: ```swift theme={"dark"} Lerix.feedback.show { result in print(result.submitted, result.id ?? "queued") } ``` The flow is message, labels, screenshots (tap one to draw on it with PencilKit, or **Add another screenshot** to capture a different screen), email, then a review where **Show details** lists everything that is sent. `show` presents from the top-most view controller of the key window; pass `from:` to choose another. ### Options Labels, the email mode and the screenshot mode come from the dashboard. Override them for one call: ```swift theme={"dark"} Lerix.feedback.show(options: LerixFeedbackOptions( labels: ["bug", "improvement"], // offer only these label keys email: .required, // .hidden, .optional, .required screenshot: .optional, // .hidden to skip screenshots custom: ["plan": "pro"] )) Lerix.feedback.setUserEmail(user.email) // prefill the email step ``` ### Shake to report Open the feedback flow when the user shakes the device. It's off until you turn it on: ```swift theme={"dark"} Lerix.feedback.enableShakeToReport() // Later, to stop: Lerix.feedback.disableShakeToReport() ``` A view controller that handles shakes itself without calling `super` keeps them from reaching the SDK. ### Hide sensitive views ```swift theme={"dark"} cardNumberLabel.lerixConfidential = true // UIKit Text(balance).lerixConfidential() // SwiftUI ``` ### Your own UI ```swift theme={"dark"} let result = try await Lerix.feedback.submit( message: "Checkout button does nothing", labels: ["bug"], email: "user@example.com", screenshots: [pngData] // PNG, JPEG or WebP, up to 5 MB each ) ``` `submit` throws for an empty message or an invalid email. Offline, the feedback is kept on the device and `result.id` is `nil` until it is sent. ### Language and colours The UI ships in English and Arabic (right to left) and follows the app's language and light or dark mode. ```swift theme={"dark"} Lerix.ui.locale = "ar" // default: the app's language Lerix.ui.tintColor = .purple // default: the key window's tint Lerix.ui.setStrings(["feedback.title": "Report a problem"], locale: "en") ``` ## NPS survey ```swift theme={"dark"} // For example after a successful checkout Task { await Lerix.surveys.maybeShowNps() } // While testing: skip the eligibility rules Task { await Lerix.surveys.maybeShowNps(force: true) } ``` `maybeShowNps` returns `true` when the survey was shown. The sheet (score, follow-up, thanks) opens at half height. The SDK shows it only when this install is due (see [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown)); with `debugMode: true` it logs why a survey was skipped. ## Analytics ```swift theme={"dark"} Lerix.track("checkout_started", props: ["items": 3, "coupon": "EID"]) Lerix.screen("Checkout") Lerix.analytics.flush() // send now Lerix.analytics.setEnabled(false) // opt out: stops recording, clears the queue, persisted ``` `$first_open`, `$session_start` / `$session_end`, `$app_update` and `$screen_view` are recorded automatically. Launches in the background (a silent push, a background fetch) don't count as a session until the app comes to the foreground. ### Screen views In SwiftUI, name each screen with `.lerixScreen`: ```swift theme={"dark"} struct CheckoutView: View { var body: some View { Form { /* ... */ } .lerixScreen("Checkout") } } ``` In UIKit, turn on `uikitScreenTracking` in `LerixAnalyticsOptions` to record a screen view each time a view controller appears. Rename a controller, or skip it with an empty string: ```swift theme={"dark"} override func viewDidLoad() { super.viewDidLoad() lerixScreenName = "Checkout" // "" skips this controller } ``` > [!NOTE] > > Call [`Lerix.setUser`](/modules/users-identity) after sign-in. Analytics > then counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs The last 50 breadcrumbs are sent with feedback and with handled error reports. Screen views are added automatically. ```swift theme={"dark"} Lerix.addBreadcrumb("Tapped pay", category: .ui, data: ["amount": 49]) ``` Categories: `.navigation`, `.log`, `.http`, `.ui`, `.custom`. # iOS (Swift) — Installation Source: https://docs.lerix.dev/frameworks/ios/installation Add Lerix to your native iOS app in a few steps — no Flutter required. ## Requirements * iOS 15+ or macOS 12+ (from 1.5.1) * Xcode 15+, Swift 5.9+ * Swift Package Manager or CocoaPods > [!NOTE] > > This is the native Swift package for apps with no Flutter involved. If your app is built with Flutter, use the [Flutter SDK](/frameworks/flutter/installation) instead — it already covers iOS, Android, and Web from one package. ## 1. Add the package In Xcode: **File → Add Package Dependencies…** and enter the package's Git URL, or add it directly to your `Package.swift`: ```swift Package.swift theme={"dark"} dependencies: [ .package(url: "https://github.com/lerixDev/lerix_ios", from: "1.1.0") ] ``` Then add `Lerix` as a dependency of your app target. ### Or with CocoaPods ```ruby Podfile theme={"dark"} pod 'Lerix', '~> 1.1' ``` Run `pod install` and open the generated `.xcworkspace`. ## 2. Initialize the SDK Call `Lerix.initialize()` as early as possible — `application(_:didFinishLaunchingWithOptions:)` in your `AppDelegate`. This registers your app/device with the backend and installs automatic crash reporting. ```swift AppDelegate.swift theme={"dark"} import UIKit import Lerix class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Lerix.initialize( apiKey: "your-api-key", projectId: "your-project-id", debugMode: true ) return true } } ``` > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. If your app uses the SwiftUI `App` lifecycle instead of a classic `AppDelegate`, attach one with `@UIApplicationDelegateAdaptor`: ```swift theme={"dark"} @main struct MyApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `apiKey` | `String` | Yes | Your project API key from the dashboard | | `projectId` | `String` | Yes | Your project ID (slug) from the dashboard | | `url` | `String` | No | Backend base URL. Defaults to `https://api.lerix.dev/v1` — only change this if you're pointing at a self-hosted backend | | `debugMode` | `Bool` | No | Print SDK request/response logs to the console (default: `false`) | | `enableCrashReporting` | `Bool` | No | Install the automatic crash handler (default: `true`) — see [Error Tracking](/frameworks/ios/error-tracking) | | `onError` | `((Error) -> Void)?` | No | Called if initialization itself fails (e.g. network unreachable) | ## 3. Verify the integration Throw a test error to confirm everything is working: ```swift theme={"dark"} Lerix.throwError("Test error", stack: Thread.callStackSymbols) ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/ios/error-tracking) to report custom errors and enable automatic crash capture, or [Push Notifications](/frameworks/ios/push-notifications) to enable push delivery. # iOS (Swift) — Push Notifications Source: https://docs.lerix.dev/frameworks/ios/push-notifications Configure Apple Push Notification service (APNs) delivery for your native iOS app. ## Introduction Lerix delivers push notifications on iOS using Apple Push Notification service (APNs) directly — no Firebase required. After you complete the [iOS installation](/frameworks/ios/installation), follow the steps below to enable push delivery in your app. > [!NOTE] > > Make sure you have initialized the SDK with `Lerix.initialize()` before configuring push notifications. ## Set up APNs authentication You need an active [Apple Developer account](https://developer.apple.com/programs/) to send push notifications on iOS. 1. In the [Apple Developer portal](https://developer.apple.com/account/resources/authkeys/list), go to **Certificates, Identifiers & Profiles → Keys**. 2. Create a new key with **Apple Push Notifications service (APNs)** enabled. 3. Download the `.p8` authentication key and note the **Key ID**. You can only download the key once. 4. Upload the `.p8` file to the Lerix dashboard, under **Notifications → Settings**. 5. In **Certificates, Identifiers & Profiles → Identifiers**, make sure your app's own App ID has the **Push Notifications** capability enabled. This is separate from the `.p8` key above and is required even though Lerix's token-based auth doesn't need a per-app certificate — APNs still rejects pushes to an App ID it has no record of. For the full setup steps, see [Apple's guide to registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns). ## Enable the capability in Xcode Add **Push Notifications** and **Background Modes → Remote notifications** under your app target's **Signing & Capabilities** tab. ## Wire up the AppDelegate ```swift AppDelegate.swift theme={"dark"} import UIKit import Lerix class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { Lerix.notifications.setDeviceToken(deviceToken) } func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { print("Failed to register for remote notifications: \(error)") } func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { Lerix.notifications.handleRemoteNotification(userInfo: userInfo) completionHandler(.newData) } } ``` ## Request permission and observe notifications ```swift theme={"dark"} Task { let granted = await Lerix.notifications.requestPermissions() } Lerix.notifications.setOnNotificationReceived { payload in // Fired while the app is open (foreground) — show an in-app banner, etc. print("Notification received: \(payload.title ?? "") — \(payload.body ?? "")") } Lerix.notifications.setOnNotificationTapped { payload in // Fired when the user taps the notification — including one that // launched the app from a fully closed state (see the note below). print("Notification tapped: \(payload.metadata)") } let status = await Lerix.notifications.checkPermissionStatus() // .authorized, .denied, .notDetermined, .provisional ``` Register `setOnNotificationTapped` somewhere with access to your navigation stack, since a tap almost always means routing to a specific screen based on `payload.metadata` rather than just logging it. > [!NOTE] > > A tap is delivered reliably even when it's what launched the app from a fully > terminated state — call `setOnNotificationTapped` as soon as your root view > appears, and any tap that happened before your app finished starting up is > replayed automatically once the callback is registered. ## The notification payload `setOnNotificationReceived` and `setOnNotificationTapped` both receive a `LerixNotificationPayload`: ```swift theme={"dark"} public struct LerixNotificationPayload { public let notificationId: String? // this notification's id in Lerix public let title: String? public let body: String? public let imageUrl: String? // whatever you passed as `imageUrl` when sending, if any public let metadata: [String: Any] // whatever you passed as `metadata` when sending } ``` ```swift theme={"dark"} Lerix.notifications.setOnNotificationTapped { payload in let screen = payload.metadata["screen"] as? String let orderId = payload.metadata["orderId"] as? String // Navigate based on payload.metadata, etc. } ``` ## Device identifiers Three different methods return three different things — use the right one for what you're doing: | Method | Returns | Use it for | | - | - | - | | `getRegisteredTokenId()` | Lerix's own id for this registered device, assigned once `requestPermissions()` succeeds and the token is registered with the backend | The value to put in **device tokens** when sending a notification to this specific device — from the dashboard's **Send notification** tab, or the [REST API](/api-reference/push-notifications) | | `getDeviceToken()` | The raw APNs device token (hex-encoded) | Rarely needed directly; useful for diagnostics or if you're calling APNs yourself outside of Lerix | | `getDeviceId()` | The device's `UIDevice.identifierForVendor` — a local iOS system identifier, unrelated to Lerix's backend | Local diagnostics only; **not** what the dashboard's send flow expects | ```swift theme={"dark"} let tokenId = Lerix.notifications.getRegisteredTokenId() print("Send test notifications to: \(tokenId ?? "not registered yet")") ``` > [!WARNING] > > `getRegisteredTokenId()` is the iOS equivalent of the Flutter SDK's > `getDeviceId()` — the naming differs between the two SDKs, but both return > the same kind of value: the id the dashboard and REST API expect in their > device-targeting field. Pasting this SDK's `getDeviceId()` (the vendor > identifier) into the dashboard's send flow will fail with a "not found" > error — it was never registered with the backend. All three return `nil` until notification permission is granted and registration completes. ## Sending a custom sound Set `sound` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`sound` parameter](/api-reference/push-notifications) — to play a bundled sound file instead of the device default. * The file must already ship inside your app's bundle — Lerix only passes the filename through, it never hosts or uploads sound files. * Pass the filename with its extension, e.g. `sound.mp3` (`.wav`/`.caf` also supported). No SDK code is required to play the sound — it happens automatically as part of the system notification. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the device inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```swift theme={"dark"} Lerix.notifications.setOnNotificationTapped { payload in let chatId = payload.metadata["chatId"] as? String // Navigate to the chat screen, etc. } ``` ## Sending an image Set `imageUrl` when sending a notification — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications) — to show a rich image in the notification. APNs has no native "image" field, so this requires a one-time setup step: 1. In Xcode: **File → New → Target… → Notification Service Extension**. Name it (e.g. `NotificationServiceExtension`). 2. Copy the package's `NotificationServiceExtension/NotificationService.swift` into your new extension target, replacing the generated one. 3. Build and run — no other configuration needed. The backend automatically sets APNs' `mutable-content` flag whenever a notification carries an `imageUrl`, which is what triggers your extension to run. > [!NOTE] > > If the extension isn't set up, an `imageUrl` is silently ignored — the > notification still shows normally with no image, it doesn't fail to send. The image is also available in Swift as `payload.imageUrl` (in `setOnNotificationReceived`/`setOnNotificationTapped`) if you want to show it in a custom in-app banner, in addition to the OS rendering it natively. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```swift theme={"dark"} try await Lerix.setUser("user_123") // after login try await Lerix.clearUser() // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```swift theme={"dark"} try await Lerix.notifications.subscribeToTopic("promotions") try await Lerix.notifications.unsubscribeFromTopic("promotions") ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. # Release control in Swift Source: https://docs.lerix.dev/frameworks/ios/release-control Show update prompts, required updates and maintenance screens in your native iOS or macOS app, or build your own. Release control works on iOS, iPadOS and macOS (Mac Catalyst apps count as macOS). For what it does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default once `Lerix.initialize` runs, with the built-in screens. To change it, pass `appControl`: ```swift AppDelegate.swift theme={"dark"} Lerix.initialize( apiKey: "your-api-key", projectId: "your-project-id", appControl: LerixAppControlOptions( enabled: true, // false: no checks, no screens, no events ui: true // false: draw your own screens ) ) ``` | Option | Default | Description | | - | - | - | | `enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | ### Version The SDK sends `CFBundleShortVersionString` as the version and `CFBundleVersion` as the build number, and the bundle ID so Lerix can pick the right store link. Rules compare build numbers when both sides have one. ### Instant changes with silent push When you start or end a maintenance window or force an update, Lerix sends a silent push so apps in the background refresh too. It needs the setup from [Push Notifications](/frameworks/ios/push-notifications): the **Remote notifications** background mode, and `didReceiveRemoteNotification` forwarding to `Lerix.notifications.handleRemoteNotification(userInfo:)`. The SDK recognises the refresh message and checks right away. Without push, apps pick up the change at their next check. ## Built-in screens * **Maintenance**: on iOS, a window of its own above everything else in the app, including alerts, with a **Try again** button. On macOS, an opaque layer over every app window that takes all clicks and keys; the menu bar (for example **Quit**) stays usable. * **Required update**: the same full-screen host, with release notes and an **Update** button that opens the App Store. * **Update prompt**: an alert (macOS: a sheet) with **Update**, **Later** and, when allowed, **Skip this version**. Release notes are added to the message. Buttons and server copy follow the app's language (English, or Arabic right to left). See [Custom UI](/modules/release-control/custom-ui#built-in-screens) for when each one appears. ## Your own screens Set `ui: false`, then read `Lerix.appControl.state` and observe changes. The state has `update` (`LerixAppUpdate`), `maintenance` (`LerixMaintenance`) and `sdk` (`LerixSdkControl`), each `nil` when nothing applies (fields in [The state](/modules/release-control/custom-ui#the-state)). Changes arrive on the main thread. ```swift theme={"dark"} Lerix.appControl.onChange = { state in if let maintenance = state.maintenance { showMaintenance(title: maintenance.title, message: maintenance.message) } else if let update = state.update, update.state == .required { showForceUpdate(message: update.message) } else { hideReleaseScreens() } } ``` `onChange` holds one closure. To observe from several places, use `addListener`, which returns a token for `removeListener`: ```swift theme={"dark"} let token = Lerix.appControl.addListener { state in /* ... */ } Lerix.appControl.removeListener(token) ``` The helpers drive the buttons: ```swift theme={"dark"} await Lerix.appControl.refresh() // Try again Lerix.appControl.openStore() // Update; returns whether the link opened Lerix.appControl.later() // hide for remindAfterHours Lerix.appControl.skip() // hide until a newer version (optional updates only) ``` ## Check now ```swift theme={"dark"} let state = await Lerix.appControl.refresh() // Without async/await Lerix.appControl.refresh { state in /* ... */ } ``` `refresh()` ignores the saved answer's lifetime, fetches, and returns the new state. The SDK already checks on launch, when the app returns to the foreground, and every minute during an emergency (see [Delivery and offline behaviour](/modules/release-control/delivery-offline)). # Laravel Source: https://docs.lerix.dev/frameworks/laravel/installation Server-side Lerix integration for Laravel — coming soon. Coming Soon A Laravel SDK for server-side error tracking and push notification sending is on the roadmap, but not available yet. Check back soon for setup instructions. In the meantime, you can call the [REST API](/api-reference/introduction) directly from any Laravel service. # NestJS — Error Tracking Source: https://docs.lerix.dev/frameworks/nestjs/error-tracking Automatic reporting of unhandled exceptions in NestJS, plus manual reporting with type, severity and metadata. ## Introduction After you complete the [NestJS installation](/frameworks/nestjs/installation), unhandled exceptions are reported automatically. Inject `LerixService` to report handled exceptions yourself with extra context. ## Automatic capture `LerixModule` registers two things: * **A global exception filter.** Every exception thrown from a controller, guard, pipe or interceptor passes through it. By default it reports exceptions that are not an `HttpException`, and `HttpException`s with a status of 500 or above. Expected client errors such as `NotFoundException` or a failed `ValidationPipe` are not reported. The filter then hands the exception to Nest's default filter, so the HTTP response is exactly what it would have been without Lerix. * **Process-level handlers.** `uncaughtException` and `unhandledRejection` are reported, the report is given up to `flushTimeoutMs` to leave, and the process exits with code 1 as Node would. Set `exitOnUnhandled: false` to keep the process alive, or `captureUnhandled: false` to skip the handlers. Every automatic report carries the request method, URL, route, status code, and the `x-request-id` (or `x-correlation-id`) header when present, under `metadata.request`. ### Change what is reported `shouldCapture` receives the exception and a context object with the request details: ```ts app.module.ts theme={"dark"} import { HttpException } from '@nestjs/common'; LerixModule.forRoot({ apiKey, projectId, shouldCapture: (exception, context) => { if (context.request?.route === '/health') return false; if (exception instanceof HttpException) return exception.getStatus() >= 500; return true; }, }); ``` ### Use your own filter If your app already registers a catch-all `APP_FILTER`, set `captureHttpExceptions: false` and report from that filter: ```ts all-exceptions.filter.ts theme={"dark"} @Catch() export class AllExceptionsFilter extends BaseExceptionFilter { constructor(adapterHost: HttpAdapterHost, private readonly lerix: LerixService) { super(adapterHost.httpAdapter); } catch(exception: unknown, host: ArgumentsHost) { void this.lerix.captureException(exception); super.catch(exception, host); } } ``` You can also extend `LerixExceptionFilter` and override `catch` to customise the response while keeping the reporting. ## Filtering errors Requires `@lerix-dev/lerix-nestjs` 1.4.0 or later. Two options decide what leaves the process. They apply to every report, whatever its origin: the global filter, uncaught exceptions and unhandled rejections, and `captureException` / `captureMessage`. Use them for message-based filtering; use `shouldCapture` (above) to decide by exception or request. ```ts app.module.ts theme={"dark"} import { LerixErrorEvent, LerixModule } from '@lerix-dev/lerix-nestjs'; LerixModule.forRoot({ apiKey, projectId, // A string matches when it appears anywhere in the message (case-insensitive); // a RegExp matches when it finds a match in the message. Blank strings are skipped. ignoreErrors: ['ECONNRESET', /^AbortError:/], // Runs after ignoreErrors. Return the event (changed or not) to send it, or null to drop it. beforeSend: (event: LerixErrorEvent) => { if (event.metadata?.['tenant'] === 'load-test') return null; return { ...event, message: event.message.replace(/\b\d{6,}\b/g, '') }; }, }); ``` * `ignoreErrors` runs first, then `beforeSend` sees only what's left. * The message matched and passed to `beforeSend` is the issue title shown in the dashboard, for example `TypeError: Cannot read properties of undefined`. * The event has `message`, `stack` (`at ...` lines), `type` (`BugType`), `severity` (`BugSeverity`) and `metadata` (what you passed to the capture call, or the request context from the filter). Device, app and runtime details are added after the hook. * If `beforeSend` throws, the original event is sent and the error is only logged. It is never reported or rethrown. * A dropped report does not pin its request's trace. `HttpException`s below 500 are already skipped by the default `shouldCapture`. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). ## Report an error manually ```ts orders.service.ts theme={"dark"} import { Injectable } from '@nestjs/common'; import { BugSeverity, BugType, LerixService } from '@lerix-dev/lerix-nestjs'; @Injectable() export class OrdersService { constructor(private readonly lerix: LerixService) {} async reconcile(orderId: string) { try { await this.ledger.reconcile(orderId); } catch (error) { await this.lerix.captureException(error, { type: BugType.LOGIC_BUG, severity: BugSeverity.HIGH, metadata: { orderId, job: 'reconcile' }, }); throw error; } } } ``` `captureException` never throws and resolves once the report has been accepted or given up on. Use `captureMessage` for something that was not thrown: ```ts theme={"dark"} await this.lerix.captureMessage('Inventory drift detected', { severity: BugSeverity.MEDIUM, metadata: { sku: 'A-1', expected: 10, actual: 7 }, }); ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `error` / `message` | `unknown` / `string` | Yes | The caught exception, or a message | | `type` | `BugType` | No | Bug classification (default `RUNTIME_ERROR` for exceptions, `LOGIC_BUG` for messages) | | `severity` | `BugSeverity` | No | Severity level (default `HIGH` for exceptions, `MEDIUM` for messages) | | `metadata` | `Record` | No | Additional JSON context attached to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception thrown at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't throw | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness or excessive resource use | | `COMPATIBILITY` | Breaks on a specific runtime, OS or platform | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The process terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## What is attached to a report | Field | Value | | - | - | | Device | Hostname, CPU architecture, OS type and release, time zone | | App | Service name and id from `package.json` (or the `app` option), version and build number | | Metadata | `environment`, Node.js version and process id, plus anything you pass in `metadata` | ## TypeScript file and line numbers Stack traces are mapped back to your TypeScript source automatically. When it reports an error, the SDK reads the `.js.map` files that `nest build` writes next to the compiled code, so a frame like `dist/src/orders/orders.service.js:58` is reported as `src/orders/orders.service.ts:42`. There is nothing to upload, no Node.js flag and nothing to configure. Nest enables `"sourceMap": true` in `tsconfig.json` by default. Inline source maps and `nest build --webpack` bundles work too. Paths are made relative to the app's working directory (normally the repository root), so they read like repository paths. Frames from dependencies show as `node_modules/...`. > [!WARNING] > > If the `.js.map` files are not deployed, frames keep their compiled > position. This happens, for example, with a Docker image that copies only > `.js` files. Keep the `.js.map` files next to `dist/**/*.js` in the image. ## Other `LerixService` methods | Method | Description | | - | - | | `getUserId()` | Anonymous user id this service is registered as | | `flush(timeoutMs?)` | Wait for in-flight reports | | `deleteUser()` / `reRegisterUser()` | Reset the anonymous user, for example after pointing the service at a different project | ## Disable in tests Pass `enabled: false` to turn every call into a no-op without changing your code: ```ts theme={"dark"} LerixModule.forRoot({ apiKey: 'x', projectId: 'x', enabled: process.env.NODE_ENV !== 'test' }); ``` # NestJS — Analytics and Feedback Source: https://docs.lerix.dev/frameworks/nestjs/feedback-analytics Track server-side events in the same funnels as your apps, and send feedback from your own backend to the Feedback inbox. Requires `@lerix-dev/lerix-nestjs` 1.5.0 or later. Both features use the `LerixService` you inject after [Installation](/frameworks/nestjs/installation). For what you see in the dashboard, read [Analytics](/modules/analytics) and [Feedback](/modules/feedback). ## Analytics Record what only your backend knows, such as a payment that went through. The event lands in the same funnels, event lists and user counts as the events your apps send: ```ts orders.service.ts theme={"dark"} import { Injectable } from '@nestjs/common'; import { LerixService } from '@lerix-dev/lerix-nestjs'; @Injectable() export class OrdersService { constructor(private readonly lerix: LerixService) {} async markPaid(order: Order) { // ... this.lerix.track('payment_succeeded', { plan: 'pro', amount: 49 }, { userId: order.userId }); } } ``` * `userId` is your own ID for the user, the same one your apps pass to [`setUser()`](/modules/users-identity). Leave it out for events that aren't about a user. * Names start with a letter, then letters, digits, spaces, `_`, `.`, `:` or `-` (2 to 64 characters). Up to 25 properties, with string (up to 1,024 characters), finite number, boolean or `null` values. An invalid name drops the event; an invalid property is dropped on its own. `track()` never throws. * Server events record no sessions or screens. ### Delivery Events are buffered in memory (up to 1,000, oldest dropped first) and sent in batches of up to 100: every 10 seconds, as soon as 50 are waiting, on `this.lerix.analytics.flush()`, and on shutdown. Call `app.enableShutdownHooks()` in `main.ts` so a graceful shutdown flushes them. When the API is unreachable or answers 401, 429 or 5xx, events are kept and retried with backoff (2 seconds up to 5 minutes). A batch refused as invalid is dropped. Nothing is written to disk, so events still pending when the process is killed are lost. ### Environment and opt-out Events carry `platform: "server"`, your app's version and build number, and the environment: the `environment` option, or `NODE_ENV`, with `production` sent as `prod` and `development` as `dev` (`prod` when unset). ```ts theme={"dark"} this.lerix.analytics.setEnabled(false); // stop recording and clear the buffer ``` `analytics: false` in the `LerixModule.forRoot()` options turns analytics off from the start. ## Feedback Send feedback your own backend collects, such as a support form, a chatbot or an email gateway, to the project's **Feedbacks** inbox: ```ts theme={"dark"} const id = await this.lerix.feedback.submit({ message: 'Checkout button does nothing', email: 'user@example.com', // optional, used for email replies labels: ['bug'], // label keys configured for the project userId: user.id, // optional, your ID for the user custom: { plan: 'pro' }, // optional, up to 50 keys / 16 KB attachments: [{ data: pngBytes }], // optional, up to 5 PNG/JPEG/WebP images of 5 MB }); ``` * Attachments are uploaded first, then the feedback is created with their IDs. * A failed request is retried twice with the same client ID, so a retry never creates a duplicate. * `submit()` rejects with `LerixApiException`: code `INVALID_FEEDBACK` for input that can't be sent (an empty or too long message, an invalid email, an attachment that isn't a PNG, JPEG or WebP image), or the API's error otherwise. With `enabled: false` it resolves `null`. * Inside a [traced request](/frameworks/nestjs/request-tracing), the feedback is linked to the request's trace. > [!NOTE] > > The `userId` you pass is stored as *reported* by your server. Replies by push > only go to users verified through a client SDK's `setUser`. # NestJS — Installation Source: https://docs.lerix.dev/frameworks/nestjs/installation Add the Lerix NestJS SDK to your backend to report unhandled exceptions and send push notifications from your server. ## Introduction `@lerix-dev/lerix-nestjs` is the server-side Lerix SDK for NestJS. Once the module is registered, every unhandled exception in your API is reported to the dashboard with its stack trace, host, release and request context, and your services get a typed client for the push notifications REST API. > [!NOTE] > > Create a project with the **NestJS** framework in the [dashboard](https://app.lerix.dev) > and copy its **API key** and **project id** from **Project Settings**. ## Requirements | | Minimum | | - | - | | Node.js | 18 | | NestJS | 10 | | TypeScript | 5 | The package has no runtime dependencies and works with both the Express and Fastify adapters. ## 1. Install the package ```bash theme={"dark"} npm install @lerix-dev/lerix-nestjs ``` ## 2. Register the module Import `LerixModule` once in your root module: ```ts app.module.ts theme={"dark"} import { Module } from '@nestjs/common'; import { LerixModule } from '@lerix-dev/lerix-nestjs'; @Module({ imports: [ LerixModule.forRoot({ apiKey: process.env.LERIX_API_KEY!, projectId: process.env.LERIX_PROJECT_ID!, environment: process.env.NODE_ENV, }), ], }) export class AppModule {} ``` Enable shutdown hooks so in-flight reports are flushed when the process is stopped: ```ts main.ts theme={"dark"} const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); await app.listen(3000); ``` That is the whole setup. The module registers a global exception filter and process-level handlers, so unhandled errors are reported from now on. See [Error Tracking](/frameworks/nestjs/error-tracking) for what gets captured and how to report errors yourself. ### Load options from `ConfigService` `forRootAsync` accepts the same options plus `imports` and `inject`: ```ts app.module.ts theme={"dark"} LerixModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ apiKey: config.getOrThrow('LERIX_API_KEY'), projectId: config.getOrThrow('LERIX_PROJECT_ID'), environment: config.get('NODE_ENV'), }), }); ``` ## 3. Verify the integration Add a route that throws and call it once: ```ts app.controller.ts theme={"dark"} @Get('lerix-test') lerixTest() { throw new Error('Lerix test error'); } ``` The client receives Nest's normal `500` response and the error appears in your [dashboard](https://app.lerix.dev) within seconds. Remove the route afterwards. ## Options | Option | Default | Description | | - | - | - | | `apiKey` | required | Project API key | | `projectId` | required | Project id | | `url` | `https://api.lerix.dev/v1` | API base URL | | `environment` | `NODE_ENV` | Sent as metadata with every report | | `app` | from `package.json` | `{ id, name, version, buildNumber }` used to identify this service | | `enabled` | `true` | `false` turns the SDK into a no-op, useful in tests | | `debug` | `false` | Log requests and reports through the Nest logger | | `captureHttpExceptions` | `true` | Register the global exception filter | | `shouldCapture` | 5xx and non-HTTP | `(exception, context) => boolean` deciding what the filter reports | | `captureUnhandled` | `true` | Report `uncaughtException` / `unhandledRejection` | | `exitOnUnhandled` | `true` | Exit with code 1 after reporting one, as Node would | | `statePath` | `.lerix/state.json` | Where the anonymous user id is persisted; `false` keeps it in memory | | `userId` | | Pin the anonymous user id instead of registering one | | `notifications` | | `{ projectId, apiKey }` enabling the [push notifications client](/frameworks/nestjs/push-notifications) | | `flushTimeoutMs` | `2000` | How long shutdown waits for pending reports | ## How your service appears in the dashboard The SDK registers your service as a `server` app. The app id defaults to the `name` in `package.json` and the release to its `version`, so reports are grouped per service and per release. Override them with the `app` option when you deploy several instances of the same code base or want a git SHA as the build number: ```ts theme={"dark"} LerixModule.forRoot({ apiKey, projectId, app: { id: 'orders-api', version: process.env.APP_VERSION, buildNumber: process.env.GIT_SHA }, }); ``` An anonymous user is registered on first boot and stored in `.lerix/state.json` under the working directory, so restarts reuse it. Add that folder to `.gitignore`, or mount it in containers. Pass `statePath: false` to skip persistence, or `userId` to control the id yourself. ## Next steps > **[Error Tracking](/frameworks/nestjs/error-tracking)** > > What the filter reports, how to change the policy, and manual reporting. > **[Push Notifications](/frameworks/nestjs/push-notifications)** > > Send, schedule, update and unsend notifications from your backend. # NestJS — Push Notifications Source: https://docs.lerix.dev/frameworks/nestjs/push-notifications Send, schedule, update and unsend push notifications from your NestJS backend with a typed client for the Lerix REST API. ## Introduction A backend does not receive push notifications, it sends them. The NestJS SDK wraps the [push notifications REST API](/api-reference/push-notifications) in a typed client so your services can notify the users of your mobile and web apps without hand-writing HTTP calls. ## 1. Create a private key Notifications are sent to the devices of the project your **app** uses, which is usually a different project from the one your backend reports errors to. In that project's **Project Settings**, create a **private key** with the *send push notifications* permission. > [!WARNING] > > A private key can send notifications to all of a project's users. Keep it > in an environment variable on the server and never ship it to a client. ## 2. Configure the client Pass the target project id and the private key as `notifications`: ```ts app.module.ts theme={"dark"} LerixModule.forRoot({ apiKey: process.env.LERIX_API_KEY!, projectId: process.env.LERIX_PROJECT_ID!, notifications: { projectId: process.env.MOBILE_PROJECT_ID!, apiKey: process.env.MOBILE_PROJECT_PRIVATE_KEY!, }, }); ``` The client is then available as `LerixService.notifications`. ## 3. Send a notification ```ts orders.service.ts theme={"dark"} const { notificationId } = await this.lerix.notifications.send({ title: 'Order shipped', body: 'Your order #1234 is on its way', deviceTokens: [user.lerixDeviceId], metadata: { orderId: '1234' }, }); ``` `deviceTokens` are the Lerix device ids your client apps obtain from `getDeviceId()` and send to your backend at login. Save the returned `notificationId`, every other method takes it. ### Broadcast to a topic ```ts theme={"dark"} await this.lerix.notifications.send({ title: 'Weekend sale', body: '20% off everything until Sunday', sendByTopic: true, topic: 'promotions', }); ``` ### Schedule for later ```ts theme={"dark"} await this.lerix.notifications.send({ title: 'Reminder', body: 'Your appointment is in one hour', deviceTokens: [deviceId], sentAt: new Date(appointment.getTime() - 3600_000), }); ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `title` | `string` | Yes | Notification title | | `body` | `string` | Yes | Notification body | | `deviceTokens` | `string[]` | Unless `sendByTopic` | Lerix device ids to deliver to | | `sendByTopic` | `boolean` | No | Deliver to every device subscribed to `topic` instead | | `topic` | `string` | When `sendByTopic` | Topic key | | `sound` | `string` | No | Custom sound file bundled in the client app | | `imageUrl` | `string` | No | Public `https://` image shown as a rich image | | `metadata` | `object` | No | Delivered to the client as `payload.metadata` | | `sentAt` | `string \| Date` | No | ISO 8601 time or `Date` to schedule delivery | Platform notes for `sound` and `imageUrl` are in the [REST API reference](/api-reference/push-notifications). ## 4. Manage a sent notification ```ts theme={"dark"} // Change the content. Scheduled ones are edited in place; sent ones are redelivered. await this.lerix.notifications.update(notificationId, { title: 'Delivered', body: 'Enjoy your order!' }); // Remove an already-delivered notification from devices. await this.lerix.notifications.unsend(notificationId); // Stop a scheduled notification before it goes out. await this.lerix.notifications.cancel(notificationId); // Per-recipient delivery outcome as reported by APNs, FCM or the browser push service. const deliveries = await this.lerix.notifications.deliveries(notificationId); ``` | Method | REST endpoint | | - | - | | `send(input)` | [`POST /notifications/send`](/api-reference/push-notifications) | | `update(id, { title, body })` | [`POST /notifications/{id}/update`](/api-reference/update-notification) | | `unsend(id)` | [`POST /notifications/{id}/unsend`](/api-reference/unsend-notification) | | `cancel(id)` | [`POST /notifications/{id}/cancel`](/api-reference/cancel-notification) | | `deliveries(id)` | [`GET /notifications/{id}/deliveries`](/api-reference/notification-deliveries) | ## Errors Every method throws `LerixApiException` when the API rejects the request. `code` is the backend's error code and `status` the HTTP status: ```ts theme={"dark"} import { LerixApiException } from '@lerix-dev/lerix-nestjs'; try { await this.lerix.notifications.cancel(id); } catch (error) { if (error instanceof LerixApiException && error.code === 'CANNOT_CANCEL_SENT_NOTIFICATION') { // already delivered } } ``` Invalid input such as a missing `topic` with `sendByTopic` is rejected locally with code `INVALID_INPUT` before any request is made. The full list of codes is in [Error & success codes](/developers/errors-responses). ## Several target projects Instantiate `LerixNotificationsClient` directly when one backend serves more than one app: ```ts theme={"dark"} import { LerixNotificationsClient } from '@lerix-dev/lerix-nestjs'; const rider = new LerixNotificationsClient({ projectId: 'rider-app', apiKey: process.env.RIDER_PRIVATE_KEY! }); const driver = new LerixNotificationsClient({ projectId: 'driver-app', apiKey: process.env.DRIVER_PRIVATE_KEY! }); ``` # NestJS — Request Tracing Source: https://docs.lerix.dev/frameworks/nestjs/request-tracing One span per request, PostgreSQL and outbound fetch spans, sampling, and the trace behind every error — for NestJS services. ## Introduction After the [installation](/frameworks/nestjs/installation), turn on request tracing to see, under **Requests** in the dashboard, every request your service handled with its route, duration, queries and outbound calls — and to open the exact request behind any error. See [Request Tracing](/modules/request-tracing) for what the dashboard shows. ## Enable it ```ts app.module.ts theme={"dark"} LerixModule.forRoot({ apiKey: process.env.LERIX_API_KEY!, projectId: process.env.LERIX_PROJECT_ID!, tracing: { enabled: true }, }) ``` With `forRootAsync`, also pass the static flag so the interceptor can be registered when the module is built: ```ts theme={"dark"} LerixModule.forRootAsync({ tracing: true, useFactory: (config: ConfigService) => ({ ...config.get('lerix'), tracing: { enabled: true } }), inject: [ConfigService], }) ``` Tracing is off by default; when off, the tracing code is never even loaded (it lives in a separate chunk of the package). ## What is recorded * **One server span per request**, created by a global interceptor and named after the **route template** from the controller and handler paths (`GET /users/:id`), never the URL. The application's global prefix is not part of the name. An incoming W3C `traceparent` header is continued. * **PostgreSQL** — the application's `pg` driver is patched (found from the working directory; nothing is added to your dependencies), which covers Drizzle, TypeORM, Knex and plain `pg`. Each query becomes a `client` span named after the SQL template; parameters and rows are never recorded. * **Outbound `fetch`** — every call becomes a `client` span named `METHOD host` and carries a `traceparent` header. Calls to the Lerix API itself are ignored. Either hook can be switched off: ```ts theme={"dark"} tracing: { enabled: true, instrumentations: { pg: true, fetch: false } } ``` Anything else can be traced by hand through `LerixService`: ```ts theme={"dark"} const handle = this.lerix.tracing?.tracer.startChild('cache get', 'client', { 'server.address': 'redis' }); try { await cache.get(key); handle?.end('ok'); } catch (error) { handle?.end('error', { 'error.type': (error as Error).name }); throw error; } ``` A child span needs a request to belong to: outside a traced request the hooks do nothing. ## The current trace The trace id is available anywhere in the request through `current()`, for example to put it in your own logs: ```ts theme={"dark"} import { current } from '@lerix-dev/lerix-nestjs'; this.logger.log(`handling order, trace ${current()?.traceId}`); ``` ## Sampling | Option | Default | Description | | - | - | - | | `sampleRate` | `0.1` | Share of successful requests kept in full. Decided once at the root and inherited downstream. | | `slowThresholdMs` | `2000` | A request slower than this is always kept in full. | | `maxSpansPerRequest` | `1000` | Child spans held per request until it ends. | Failed requests and requests that reported an error are always kept whole. Every request's root span is still sent, so the per-route counts and percentiles in the dashboard cover 100% of traffic. ## Errors and traces Every error report made inside a request — the automatic filter's reports as well as your own `captureException` / `captureMessage` calls — carries that request's trace, so the issue page shows the waterfall inline with the failing query highlighted. An error reported outside any request (a job, a startup check) records as before, with no trace attached. ## Client addresses Each request span carries the client's address, resolved the way the project asks under **Project settings → Tracing**: in the default `masked` mode the SDK zeroes the last IPv4 octet or truncates IPv6 to its /64 before anything is sent; in `hashed` and `full` modes the address is handled on the Lerix side. The trusted-proxy depth and header decide how it is read from `X-Forwarded-For`; the whole chain is never trusted. ## Exporter options | Option | Default | Description | | - | - | - | | `flushIntervalMs` | `5000` | How often queued spans are sent when the batch is not full | | `maxQueueSize` | `2048` | Spans held in memory before new ones are dropped | | `maxBatchSize` | `512` | Spans per export request | | `requestTimeoutMs` | `10000` | Timeout of one export request | Spans leave as OTLP/HTTP JSON. If the Lerix API is unreachable the batch is counted and dropped; nothing is retried, nothing blocks a request, nothing throws into your code. Counters are on `LerixService.tracing?.stats()`. ## Overhead Measured on a trivial route with 20 000 sequential requests: about **+30 µs mean per request** (p50 +20 µs), plus about 5 µs per query and 8 µs per outbound call for the hooks. # Next.js — Error Tracking Source: https://docs.lerix.dev/frameworks/nextjs/error-tracking Report errors and automatically capture uncaught exceptions from your Next.js app to the Lerix dashboard. ## Introduction Lerix captures errors from your Next.js app so you can debug issues faster. After you complete the [Next.js installation](/frameworks/nextjs/installation), use `useLerix().throwError()` to send custom errors to your dashboard — uncaught errors are captured automatically, with no extra code required. > [!NOTE] > > Make sure your app is wrapped in `LerixNextProvider` before reporting errors. ## Report an error ```tsx theme={"dark"} 'use client'; import { useLerix, BugSeverity } from '@lerix-dev/lerix-nextjs'; export function MyComponent() { const lerix = useLerix(); const onSomethingFailed = async () => { try { await riskyOperation(); } catch (error) { lerix.throwError( String(error), undefined, undefined, BugSeverity.HIGH, { userId: '123', action: 'payment', amount: 100 }, ); } }; } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `issue` | `string` | Yes | Error message or description | | `stack` | `string[]` | No | Stack trace frames. If omitted, one is captured automatically from the call site — see [Automatic stack capture](#automatic-stack-capture) below | | `type` | `BugType` | No | Bug classification (default: `RUNTIME_ERROR`) | | `severity` | `BugSeverity` | No | Severity level (default: `MEDIUM`) | | `metadata` | `Record` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception thrown at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't throw | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness, jank, or excessive resource use | | `COMPATIBILITY` | Breaks on a specific browser or device | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The app terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## Automatic error capture `LerixNextProvider` installs error capture two ways, so nothing needs wrapping in a `try`/`catch` for baseline coverage: * `LerixErrorBoundary` catches errors thrown during rendering — React's error boundary contract only covers the render phase, not event handlers or async code. * Global `window.onerror` and `unhandledrejection` listeners catch everything else. To disable the global listeners (`LerixErrorBoundary` still catches render errors regardless): ```tsx theme={"dark"} ``` ### Next.js's own error boundaries `LerixErrorBoundary` only catches errors below where you place it. For errors above that point — most notably in the root layout itself — Next.js requires its own file-based error boundaries (`error.tsx`, `global-error.tsx`). Call `reportCaughtError` directly inside those instead: ```tsx app/error.tsx theme={"dark"} 'use client'; import { reportCaughtError } from '@lerix-dev/lerix-nextjs'; import { useEffect } from 'react'; export default function Error({ error }: { error: Error }) { useEffect(() => { reportCaughtError(error); }, [error]); return

Something went wrong.

; } ``` ## Automatic stack capture When you call `throwError()` without a `stack` argument, one is captured automatically from the call site — you only need to pass one explicitly when reporting a caught `Error`'s own stack instead (e.g. `error.stack?.split('\n')`). Either way, every stack frame is resolved from its compiled/bundled position back to the original source file and line, using the build's own source map — including Turbopack's sectioned source map format. This happens automatically; there's nothing to configure. > [!NOTE] > > Resolution requires a source map to be reachable at the frame's URL — > always true in development, and true in production only if your build > serves its `.map` files publicly. If your production build keeps source > maps private (a common choice), frames fall back to their compiled > position rather than failing to report. > [!TIP] > > To get readable production stack traces while keeping your source maps > private, upload them to Lerix after each build. See > [Next.js source maps](/frameworks/nextjs/source-maps). ### Verify the integration ```tsx theme={"dark"} lerix.throwError('Test error'); ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds, with a resolved stack trace pointing at this exact line. ## Filtering errors Requires `@lerix-dev/lerix-nextjs` 1.3.0 or later. Two optional `LerixNextProvider` options decide which errors are sent. They apply to every report: `LerixErrorBoundary`, `reportCaughtError` in `error.tsx`, and the global handlers. Functions and RegExps can't be passed as props from a Server Component, so set these options in a small client component of your own. `apiKey` and `projectId` still come from your `NEXT_PUBLIC_` environment variables: ```tsx app/lerix.tsx theme={"dark"} 'use client'; import { LerixNextProvider } from '@lerix-dev/lerix-nextjs'; export function Lerix({ children }: { children: React.ReactNode }) { return ( (event.message.includes('extension://') ? null : event), }} > {children} ); } ``` Then use it in `layout.tsx` in place of `LerixNextProvider`: ```tsx app/layout.tsx theme={"dark"} import { LerixErrorBoundary } from '@lerix-dev/lerix-nextjs'; import { Lerix } from './lerix'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( Something went wrong.

}> {children}
); } ``` * `ignoreErrors` runs first. A string drops any error whose message contains it, ignoring case. A `RegExp` drops any error whose message it matches. Empty strings are skipped. * `beforeSend` then sees only what's left. Return the event (changed or not) to send it, or `null` to drop it. If it throws, the original event is sent and the exception never reaches your app. * The message you match against is the issue title, which includes the error class, for example `TypeError: boom`. `beforeSend` receives a `LerixErrorEvent`: `{ message, stack, type, severity, metadata? }`. `stack` is an array of frames, already resolved through your source maps. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # Next.js — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/nextjs/feedback-analytics Add an in-app feedback dialog, an NPS survey and product analytics to your Next.js app, with App Router screen tracking. Requires `@lerix-dev/lerix-nextjs` 1.4.0 or later. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). ## Setup Add `LerixScreenTracker` once, inside the `LerixNextProvider` from [Installation](/frameworks/nextjs/installation). It records a `$screen_view` per pathname through `usePathname()`, and turns off the SDK's History API tracking so nothing is counted twice. ```tsx app/layout.tsx theme={"dark"} import { LerixErrorBoundary, LerixNextProvider, LerixScreenTracker } from '@lerix-dev/lerix-nextjs'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( path.replace(/^\/products\/[^/]+/, '/products/[id]')} /> Something went wrong.

}> {children}
); } ``` The optional `name` maps a pathname to a screen name. Use it to group dynamic routes, so `/products/42` and `/products/43` count as one screen. Return `null` to skip a path. Without `LerixScreenTracker` (in the Pages Router, for example), URL changes are still recorded through the History API. ### Environment `LerixNextProvider` reads `NEXT_PUBLIC_LERIX_ENVIRONMENT` as the default environment. Without it, the SDK sends `dev` on `localhost` and `*.local` or in debug mode, and `prod` elsewhere. ```bash .env.production theme={"dark"} NEXT_PUBLIC_LERIX_ENVIRONMENT=prod ``` The provider's `options` prop also takes `locale`, `analytics`, `feedback` (`theme`, `primaryColor`, `strings`, `zIndex`) and `captureConsoleErrors`. They work as in [React](/frameworks/react/feedback-analytics#setup). ## In-app feedback ```tsx components/feedback-button.tsx theme={"dark"} 'use client'; import { useLerixFeedback } from '@lerix-dev/lerix-nextjs'; export function FeedbackButton() { const feedback = useLerixFeedback(); return ; } ``` The dialog renders inside a Shadow DOM and loads on first use, in its own chunk. `show(options?)` takes `labels`, `email`, `screenshot`, `prefillEmail` and `custom`, and `submit({ message, labels?, email?, screenshots?, custom? })` sends feedback from your own form. See [React](/frameworks/react/feedback-analytics#in-app-feedback) for the full option list. Mask sensitive content in screenshots: ```tsx theme={"dark"} 'use client'; import { LerixConfidential } from '@lerix-dev/lerix-nextjs'; {user.email} ``` ## NPS survey ```tsx app/(app)/home-client.tsx theme={"dark"} 'use client'; import { useLerixSurvey } from '@lerix-dev/lerix-nextjs'; export function HomeClient() { useLerixSurvey({ autoShow: true }); // the NPS card, when this install is due return null; } ``` Or call `maybeShowNps()` from `useLerixSurvey()` yourself, with `{ force: true }` while testing. See [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown). ## Analytics ```tsx theme={"dark"} 'use client'; import { useLerixTrack } from '@lerix-dev/lerix-nextjs'; export function CheckoutButton() { const track = useLerixTrack(); return ; } ``` * `$first_open`, `$session_start` / `$session_end`, `$app_update` and `$screen_view` are recorded automatically. * `useLerixScreen('Settings / Billing')` names a screen the URL doesn't, such as a tab or a modal step. * `Lerix.analytics.setEnabled(false)` is the persisted opt-out, and `Lerix.analytics.flush()` sends right away. `Lerix` is re-exported from `@lerix-dev/lerix-nextjs`. This package records events in the browser. For events from code on the Edge runtime, see [Edge Functions](/frameworks/edge/feedback-analytics). > [!NOTE] > > Call [`setUser`](/modules/users-identity) after sign-in. Analytics then > counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs ```ts theme={"dark"} 'use client'; import { Lerix } from '@lerix-dev/lerix-nextjs'; Lerix.addBreadcrumb('Applied coupon', 'ui', { code: 'EID' }); ``` The last 50 breadcrumbs (navigation is added automatically) are attached to feedback. # Next.js — Installation Source: https://docs.lerix.dev/frameworks/nextjs/installation Add Lerix to your Next.js app in a few steps. ## Requirements * Next.js 13+ (App Router or Pages Router) * React 18+ > [!NOTE] > > This is a dedicated Next.js library — a `LerixNextProvider` pre-marked > `'use client'`, plus the same `LerixErrorBoundary` and > `useLerix()`/`useLerixNotifications()` hooks the [React SDK](/frameworks/react/installation) > uses, built on `@lerix-dev/lerix-core`. If you're using plain React (not Next.js), > use `@lerix-dev/lerix-react` directly instead. ## 1. Add the dependency ```bash theme={"dark"} npm install @lerix-dev/lerix-nextjs ``` ## 2. Configure your API key and project ID Add to `.env.local`: ```bash .env.local theme={"dark"} NEXT_PUBLIC_LERIX_API_KEY=your-api-key NEXT_PUBLIC_LERIX_PROJECT_ID=your-project-id ``` These must be prefixed `NEXT_PUBLIC_` — that's Next.js's own convention for env vars exposed to client-side code, required here since the SDK registers/reports from the browser. > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. ## 3. Register the SDK Wrap your root layout in `LerixNextProvider` and `LerixErrorBoundary`: ```tsx app/layout.tsx theme={"dark"} import { LerixErrorBoundary, LerixNextProvider } from '@lerix-dev/lerix-nextjs'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( Something went wrong.

}> {children}
); } ``` For the **Pages Router**, do the same in `pages/_app.tsx` instead. `layout.tsx` itself stays a Server Component — `LerixNextProvider` and `LerixErrorBoundary` are pre-marked `'use client'` internally, so wrapping them around `children` doesn't force your whole tree to become client-only. `LerixNextProvider` reads `NEXT_PUBLIC_LERIX_API_KEY`/`NEXT_PUBLIC_LERIX_PROJECT_ID` automatically — pass an `options` prop explicitly only if you want to override them: ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `options.apiKey` | `string` | No | Overrides `NEXT_PUBLIC_LERIX_API_KEY` | | `options.projectId` | `string` | No | Overrides `NEXT_PUBLIC_LERIX_PROJECT_ID` | | `options.url` | `string` | No | Backend base URL. Defaults to `https://api.lerix.dev/v1` | | `options.release` | `string` | No | Overrides `NEXT_PUBLIC_LERIX_RELEASE`. The version this build ships as; pass the same value when uploading source maps: see [Source maps](/frameworks/nextjs/source-maps) | | `options.debugMode` | `boolean` | No | Print SDK request/response logs to the console (default: `false`) | | `options.enableCrashReporting` | `boolean` | No | Install the automatic error handler (default: `true`) — see [Error Tracking](/frameworks/nextjs/error-tracking) | ## 4. Verify the integration ```tsx theme={"dark"} 'use client'; import { useLerix } from '@lerix-dev/lerix-nextjs'; export function MyComponent() { const lerix = useLerix(); return ; } ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/nextjs/error-tracking) to report custom errors and enable automatic capture, or [Push Notifications](/frameworks/nextjs/push-notifications) to enable push delivery. # Next.js — Push Notifications Source: https://docs.lerix.dev/frameworks/nextjs/push-notifications Configure standard Web Push (VAPID) delivery for your Next.js app — no Firebase, no OneSignal required. ## Introduction Lerix delivers push notifications to Next.js apps using the standard **Web Push protocol (VAPID)** — the same open standard Chrome, Firefox, Edge, and modern Safari all implement natively. No Firebase project, no OneSignal account — Lerix generates and manages the key pair for you. After you complete the [Next.js installation](/frameworks/nextjs/installation), follow the steps below to enable push delivery. > [!NOTE] > > Make sure your app is wrapped in `LerixNextProvider` before configuring push notifications. ## 1. Copy the service worker Web Push requires a service worker file served from your app's own origin — a package can't add this file to your app automatically. Copy it into your `public` folder so it's reachable at your site's root: ```bash theme={"dark"} cp node_modules/@lerix-dev/lerix-core/sw/lerix-sw.js your-app/public/lerix-sw.js ``` ## 2. Generate a Web Push key From the dashboard: **Notifications → Settings → Web Push → Generate keys**. This is a one-click step — Lerix generates and stores the VAPID key pair for the project; there's nothing to copy into your code. ## 3. Subscribe ```tsx theme={"dark"} 'use client'; import { useEffect } from 'react'; import { useLerixNotifications } from '@lerix-dev/lerix-nextjs'; export function MyComponent() { const notifications = useLerixNotifications(); useEffect(() => { notifications.setOnNotificationReceived((payload) => { // Fired while a tab is open. console.log(payload.title, payload.body); }); notifications.setOnNotificationTapped((payload) => { // Fired when the user clicks the notification — including one that // opened the page from a fully closed tab. const screen = payload.metadata['screen']; }); }, []); const enablePush = async () => { const subscribed = await notifications.subscribe(); }; return ; } ``` `subscribe()` prompts for notification permission, registers the service worker, subscribes this browser to push, and registers the subscription with the backend — one call for the whole flow. ## The notification payload Both callbacks receive a `LerixNotificationPayload`: ```ts theme={"dark"} interface LerixNotificationPayload { notificationId: string | null; title: string | null; body: string | null; metadata: Record; imageUrl: string | null; } ``` ## Device identifiers | Method | Returns | Use it for | | - | - | - | | `getRegisteredTokenId()` | Lerix's own id for this registered device | The value for the dashboard's/REST API's device-targeting field | | `getDeviceToken()` | The raw `PushSubscription`, stringified | Rarely needed directly; diagnostics only | | `getDeviceId()` | A locally generated identifier, unrelated to Lerix's backend | Local diagnostics only; **not** what the dashboard's send flow expects | ```tsx theme={"dark"} const tokenId = notifications.getRegisteredTokenId(); ``` All three return `null` until `subscribe()` has completed at least once. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the page inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```tsx theme={"dark"} notifications.setOnNotificationTapped((payload) => { const chatId = payload.metadata['chatId']; }); ``` ## Sending an image Set `imageUrl` when sending — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications). This works automatically with no extra setup: the service worker renders it directly as the notification's icon/image. > [!NOTE] > > **Custom sound is not supported for web push.** The Web Notifications API > doesn't expose a way to set a custom notification sound across browsers, > so the dashboard/API's `sound` parameter is ignored for browser > recipients — it only applies to iOS and Android. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```tsx theme={"dark"} import { useLerix } from '@lerix-dev/lerix-nextjs'; const lerix = useLerix(); // inside a component await lerix.setUser('user_123'); // after login await lerix.clearUser(); // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```tsx theme={"dark"} await notifications.subscribeToTopic('promotions'); await notifications.unsubscribeFromTopic('promotions'); ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. ## Browser support Chrome, Firefox, and Edge support Web Push fully. Safari requires macOS 13+ or iOS 16.4+ — older Safari doesn't support the standard at all. # Release control in Next.js Source: https://docs.lerix.dev/frameworks/nextjs/release-control Ask users to reload for a new deployment, require it, or show a maintenance screen in your Next.js app. For what release control does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default inside the `LerixNextProvider` from [Installation](/frameworks/nextjs/installation), with the built-in screens. Set the release at build time so update rules can compare versions: ```bash .env.production theme={"dark"} NEXT_PUBLIC_LERIX_RELEASE=1.4.0 ``` Use a version with digits, such as `1.4.0`, not a commit SHA. Without a release, update rules never apply on the web. Maintenance mode works either way, unless a window is limited to certain versions. To change the defaults, pass `appControl` in the provider's `options`: ```tsx app/layout.tsx theme={"dark"} {children} ``` | Option | Default | Description | | - | - | - | | `appControl.enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `appControl.ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | ## What users see The same screens as in [React](/frameworks/react/release-control#what-users-see): a maintenance overlay with **Try again**, a required update that reloads the page at most once every 5 minutes and then shows a **Reload** button, and a dismissible bar for optional or recommended updates. ## Your own screens Set `appControl: { ui: false }`, then use `useLerixAppControl()` from a client component. It works as in [React](/frameworks/react/release-control#your-own-screens). ```tsx components/release-gate.tsx theme={"dark"} 'use client'; import { useLerixAppControl } from '@lerix-dev/lerix-nextjs'; export function ReleaseGate({ children }: { children: React.ReactNode }) { const { maintenance, update, refresh, openStore } = useLerixAppControl(); if (maintenance) { return refresh()} />; } if (update?.state === 'required') { return openStore()} />; } return <>{children}; } ``` Put `ReleaseGate` inside `LerixNextProvider` in your root layout, around `{children}`. # Next.js — Source Maps Source: https://docs.lerix.dev/frameworks/nextjs/source-maps Upload your production source maps privately so Lerix shows Next.js errors at the original file and line. ## Introduction Production Next.js builds are minified, so a stack trace from a real user looks like this: ```text theme={"dark"} onConnMessage@https://app.example.com/_next/static/chunks/app/page-Cr8GYWvZ.js:10:14780 ``` When you upload your build's source maps to Lerix, the dashboard maps each frame back to your original code, for example `app/chat/page.tsx:142`, and shows the surrounding lines. The maps are uploaded privately and deleted from your build output before you deploy. They are never served publicly, so your source code stays private. > [!NOTE] > > Complete the [Next.js installation](/frameworks/nextjs/installation) first. > Setting a release requires `@lerix-dev/lerix-core` 1.1.0 or newer. ## 1. Enable browser source maps Turn on production browser source maps in your Next.js config: ```ts next.config.ts theme={"dark"} import type { NextConfig } from 'next'; const nextConfig: NextConfig = { productionBrowserSourceMaps: true, }; export default nextConfig; ``` Run `next build` and check that `.js.map` files appear under `.next/static/chunks`. > [!NOTE] > > Next.js adds a `//# sourceMappingURL` comment to the shipped JavaScript. > That is harmless: the CLI deletes the maps before you deploy, so the comment > just points to a file that returns 404. ## 2. Create a private API key Uploading source maps needs a private key, separate from the public project key your app uses. 1. Open your project in the [Lerix Dashboard](https://app.lerix.dev). 2. Go to **Project settings** → **API keys**. 3. Create a key with the **Upload source maps** permission (or **Full access**). > [!WARNING] > > This key is secret. Store it as a CI secret and never put it in client code, > a `NEXT_PUBLIC_` variable, or your repository. ## 3. Upload the maps after each build After `next build` and before you deploy, run the Lerix CLI on the `.next` folder: ```bash theme={"dark"} npx @lerix-dev/lerix-cli upload ``` Run it from your project folder. It finds the build output on its own (`.next`) and uses the `version` from `package.json` as the release. To set them yourself: `npx @lerix-dev/lerix-cli upload .next --release 1.4.0`. The CLI finds the maps under `.next/static/chunks`, uploads every `*.js.map` file (it skips `node_modules` and CSS maps), then deletes the `.map` files from the build output so they are never deployed. If any upload fails, it keeps the maps and exits with code 1, so you can retry the step. The CLI requires Node.js 18.17 or newer. If your hosting provider runs the build for you, run the upload in the same build command so it happens before the output is deployed, for example `next build && npx @lerix-dev/lerix-cli upload`. ### Upload on every build Instead of running the command yourself, install the CLI in your project and let npm run it after each build: ```bash theme={"dark"} npm install --save-dev @lerix-dev/lerix-cli ``` ```json package.json theme={"dark"} "scripts": { "build": "next build", "postbuild": "lerix upload --optional" } ``` Now every `npm run build` uploads the maps. `--optional` keeps local builds working: on a machine without `LERIX_SECRET_KEY` (a developer's laptop), the upload is skipped instead of failing the build, and the `.map` files are still deleted. Set the keys in your CI or build server, where the release build runs. > [!NOTE] > > pnpm and Yarn 2+ don't run `postbuild` scripts. Chain the command instead: > `"build": "next build && lerix upload --optional"`. ### Options Each option can also be set with an environment variable. | Option | Environment variable | Description | | - | - | - | | `--release` | `LERIX_RELEASE` | The version this build ships as. Defaults to `version` in `package.json`. Pass the same value to the SDK (see step 4) | | `--api-key` | `LERIX_API_KEY` | Your normal public project API key | | `--project-id` | `LERIX_PROJECT_ID` | Your project ID | | `--secret-key` | `LERIX_SECRET_KEY` | The private key from step 2 | | `--url` | `LERIX_URL` | Lerix API URL. Defaults to `https://api.lerix.dev/v1` | | `--keep-maps` | | Keep the `.map` files in the build output instead of deleting them | | `--dry-run` | | List the maps that would be uploaded, without uploading anything | | `--optional` | | Skip the upload instead of failing when the keys aren't set (local builds). The `.map` files are still deleted | ## 4. Pass the same release to the SDK Set `NEXT_PUBLIC_LERIX_RELEASE` to the value you passed to `--release`. `LerixNextProvider` reads it automatically: ```bash .env.local theme={"dark"} NEXT_PUBLIC_LERIX_RELEASE=1.4.0 ``` Or pass `release` in `options` explicitly: ```tsx theme={"dark"} ``` Lerix matches maps by the script's file name. Next.js chunk names are content-hashed, so they are unique per build, and Lerix prefers maps from the same release. Matching still works if you omit `release`, but setting it is recommended. The release also replaces the `0.0.0` app version previously shown for web apps. > [!TIP] > > Use the same value in both places without editing it by hand: your git > commit SHA or the `version` from `package.json` both work well. ## Example: GitHub Actions This workflow builds the app, uploads the maps, then deploys. Add `LERIX_API_KEY`, `LERIX_PROJECT_ID` and `LERIX_SECRET_KEY` as repository secrets. ```yaml .github/workflows/deploy.yml theme={"dark"} jobs: deploy: runs-on: ubuntu-latest env: LERIX_RELEASE: ${{ github.sha }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci # Make the same release available to the app at build time - run: npx next build env: NEXT_PUBLIC_LERIX_RELEASE: ${{ github.sha }} - run: npx @lerix-dev/lerix-cli upload env: LERIX_API_KEY: ${{ secrets.LERIX_API_KEY }} LERIX_PROJECT_ID: ${{ secrets.LERIX_PROJECT_ID }} LERIX_SECRET_KEY: ${{ secrets.LERIX_SECRET_KEY }} # Deploy the build here. .next no longer contains .map files. ``` ## What you see in the dashboard New errors show the original file, line and function, with the surrounding code. Click **Show minified** to see the stack exactly as the browser sent it. The AI assistant, GitHub issues and the MCP server all receive the readable stack too. * Only errors reported after the maps are uploaded are mapped. Earlier errors keep their minified stack. * Lerix keeps the newest 5,000 maps per project. * Each map can be up to 50 MB. ## Troubleshooting | Problem | Fix | | - | - | | `no .js.map files found` | Browser source maps are not enabled. Check `productionBrowserSourceMaps` in step 1 and that you point the CLI at the `.next` folder. | | `Uploading source maps needs a private API key...` (`SOURCE_MAP_SECRET_KEY_MISSING`) | `--secret-key` is missing or invalid. Create a private key as described in step 2. | | `PROJECT_API_KEY_DONT_HAVE_PERMISSION` | The private key lacks the **Upload source maps** permission. Edit the key or create a new one with that permission. | | The stack is still minified | Check that the SDK `release` matches `--release`, that you uploaded maps from this exact build, and that the error is new. Every rebuild changes chunk hashes, so upload the maps again after each build. | # Python — Error Tracking Source: https://docs.lerix.dev/frameworks/python/error-tracking Automatic reporting of unhandled exceptions in Python, FastAPI, Flask and Django, plus manual reporting with type, severity and metadata. ## Introduction After you complete the [Python installation](/frameworks/python/installation), unhandled exceptions are reported automatically. Call `lerix.capture_exception()` to report handled exceptions yourself with extra context. ## Automatic capture `lerix.init()` installs two process-level hooks: * **`sys.excepthook`.** An exception that escapes the main thread is reported, given up to `flush_timeout` seconds to leave, and then handed to the previous hook so the traceback is still printed and the interpreter exits as it normally would. `KeyboardInterrupt` and `SystemExit` are not reported. * **`threading.excepthook`.** An exception that kills a `threading.Thread` is reported with the thread name in `metadata.thread`. Set `capture_unhandled=False` to skip both. ### asyncio Exceptions inside tasks whose result is never awaited are only logged by asyncio. Call `install_asyncio_handler` after creating the loop to report them too: ```python theme={"dark"} import asyncio import lerix async def main(): lerix.install_asyncio_handler(asyncio.get_running_loop()) ... asyncio.run(main()) ``` ### Web frameworks Frameworks turn exceptions into a `500` response before they reach the process hooks, so each one has its own hook. They all attach the request method, URL, route, status code and the `X-Request-Id` header when present, under `metadata.request`, and then let the framework's own error handling run unchanged. | Framework | Hook | Reports | | - | - | - | | FastAPI / Starlette | `app.add_middleware(LerixMiddleware)` from `lerix.integrations.fastapi` | Every exception that escapes the app; `HTTPException`s below 500 are ignored | | Flask | `LerixFlask(app)` from `lerix.integrations.flask` | Every exception passed to `got_request_exception`; `HTTPException`s below 500 are ignored | | Django | `"lerix.integrations.django.LerixMiddleware"` in `MIDDLEWARE` | Every exception reaching `process_exception`; `Http404`, `PermissionDenied` and `SuspiciousOperation` are ignored | ### Change what is reported Every hook accepts a `should_capture` callable that receives the exception and the HTTP status that will be sent (when known) and returns whether to report it: ```python main.py theme={"dark"} from lerix.integrations.fastapi import LerixMiddleware def should_capture(exc: BaseException, status_code: int | None) -> bool: if isinstance(exc, TimeoutError): return False return status_code is None or status_code >= 500 app.add_middleware(LerixMiddleware, should_capture=should_capture) ``` For Flask pass it to `LerixFlask(app, should_capture=...)`. For Django set it on the class in `settings.py`: ```python settings.py theme={"dark"} from lerix.integrations.django import LerixMiddleware LerixMiddleware.should_capture = lambda exc, status: status is None or status >= 500 ``` ### Several clients The hooks use the client created by `lerix.init()`. Pass `client=` to a hook (or set `LerixMiddleware.client` for Django) when one process reports to more than one project. ## Filtering errors Requires `lerix` 0.4.0 or later. Two options decide what leaves the process. They apply to every report, whatever its origin: the framework integrations, `sys.excepthook` / `threading.excepthook` / the asyncio handler, and `capture_exception` / `capture_message`. Use them for message-based filtering; use `should_capture` (above) to decide by exception or HTTP status. ```python main.py theme={"dark"} import re import lerix from lerix import ErrorEvent def before_send(event: ErrorEvent): if (event.metadata or {}).get("tenant") == "load-test": return None # drop it event.message = re.sub(r"\b\d{6,}\b", "", event.message) return event # send it (changed or not) lerix.init( api_key="...", project_id="...", # A string matches when it appears anywhere in the message (case-insensitive, # blank strings are skipped); a compiled pattern matches when .search() finds a match. ignore_errors=["ConnectionResetError", re.compile(r"^TimeoutError")], before_send=before_send, # runs after ignore_errors ) ``` * `ignore_errors` (`list[str | re.Pattern]`) runs first, then `before_send` sees only what's left. * The message matched and passed to `before_send` is the issue title shown in the dashboard, for example `ValueError: boom`. * `lerix.ErrorEvent` is a dataclass with `message`, `stack` (list of frame strings), `type` (`BugType`), `severity` (`BugSeverity`) and `metadata` (what you passed to the capture call, or the request context from an integration). Device, app and runtime details are added after the hook, which runs on the thread that captured the error. * If `before_send` raises, the original event is sent and the error is only logged. It is never reported or re-raised. * A dropped report does not pin its request's trace. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). ## Report an error manually ```python orders.py theme={"dark"} import lerix from lerix import BugSeverity, BugType def reconcile(order_id: str) -> None: try: ledger.reconcile(order_id) except LedgerError as error: lerix.capture_exception( error, type=BugType.LOGIC_BUG, severity=BugSeverity.HIGH, metadata={"order_id": order_id, "job": "reconcile"}, ) raise ``` Inside an `except` block you can leave the exception out and `capture_exception()` picks up the one being handled. It never raises and never blocks: the report is sent from a background thread, and the returned `Future` resolves once the report has been accepted or given up on. Use `capture_message` for something that was not raised: ```python theme={"dark"} lerix.capture_message( "Inventory drift detected", severity=BugSeverity.MEDIUM, metadata={"sku": "A-1", "expected": 10, "actual": 7}, ) ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `error` / `message` | `BaseException` / `str` | Yes | The caught exception (defaults to the one being handled), or a message | | `type` | `BugType` | No | Bug classification (default `RUNTIME_ERROR` for exceptions, `LOGIC_BUG` for messages) | | `severity` | `BugSeverity` | No | Severity level (default `HIGH` for exceptions, `MEDIUM` for messages) | | `metadata` | `dict` | No | Additional JSON context attached to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception raised at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't raise | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness or excessive resource use | | `COMPATIBILITY` | Breaks on a specific runtime, OS or platform | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The process terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## What is attached to a report | Field | Value | | - | - | | Device | Hostname, CPU architecture, OS name and release, time zone | | App | Service name and id from `pyproject.toml` (or the `app` option), version and build number | | Metadata | `environment`, Python version and implementation, process id, plus anything you pass in `metadata` | ## Flush before exit Reports are sent in the background. Long-running servers do not need to do anything, and the SDK flushes on normal interpreter exit. In short-lived scripts, workers or serverless handlers, call `flush()` before returning: ```python theme={"dark"} lerix.flush() # waits up to flush_timeout seconds lerix.flush(5.0) # or an explicit timeout ``` ## Other functions | Function | Description | | - | - | | `lerix.get_client()` | The `LerixClient` created by `init()` | | `client.get_user_id()` | Anonymous user id this service is registered as | | `client.delete_user()` / `client.re_register_user()` | Reset the anonymous user, for example after pointing the service at a different project | | `lerix.close()` | Flush, stop the background thread and remove the process hooks | `LerixClient` can also be instantiated directly, with the same options as `init()`, when you need more than one client in a process. ## Disable in tests Pass `enabled=False` to turn every call into a no-op without changing your code: ```python theme={"dark"} lerix.init(api_key="x", project_id="x", enabled=os.environ.get("ENV") != "test") ``` # Python — Analytics and Feedback Source: https://docs.lerix.dev/frameworks/python/feedback-analytics Track server-side events in the same funnels as your apps, and send feedback from your own backend to the Feedback inbox. Requires `lerix` 0.5.0 or later. For what you see in the dashboard, read [Analytics](/modules/analytics) and [Feedback](/modules/feedback). ## Analytics Record what happens on your backend as analytics events. `track` returns immediately and never raises: events are buffered in memory and a background thread sends them in batches. ```python theme={"dark"} import lerix lerix.init(api_key="...", project_id="...") lerix.track("invoice_paid", {"amount": 42.5, "currency": "OMR", "first": True}, user_id=user.id) lerix.track("nightly_export_finished") ``` * **`user_id`** is your own ID for the person the event is about, the same one your apps pass to [`setUser()`](/modules/users-identity). Events without it belong to one anonymous service user, so pass it whenever the event is about someone. * **Names** start with a letter, then letters, digits, spaces, `_`, `.`, `:` or `-` (2 to 64 characters). An event with an invalid name is dropped (and logged with `debug=True`). * **Props** are a `dict` of up to 25 keys. Keys start with a letter or `_`, then letters, digits, `_`, `.` or `-`. Values are strings (up to 1,024 characters), finite numbers, bools or `None`. Invalid entries are dropped one by one; nested dicts and lists aren't supported. ### Delivery Events are sent every 10 seconds, as soon as 50 are waiting, on `lerix.flush()` or `lerix.analytics.flush()`, and when the interpreter exits. Each request carries up to 100 events, and up to 1,000 wait in memory (the oldest is dropped past that). A batch the API refuses as invalid is dropped. A network error, 401, 429 or 5xx keeps it and retries with backoff (2 seconds up to 5 minutes). Events aren't kept across restarts. ```python theme={"dark"} lerix.init( api_key="...", project_id="...", analytics={"flush_interval": 5, "flush_at": 50, "max_batch_size": 100, "max_queue_size": 1000}, ) lerix.init(api_key="...", project_id="...", analytics=False) # track() becomes a no-op lerix.analytics.set_enabled(False) # stop recording and clear the buffer (e.g. a user opted out) lerix.analytics.flush(timeout=5) # send now; True when it finished in time ``` ### Environment Events carry `prod`, `dev` or your own value: `production` becomes `prod` and `development` becomes `dev`. An unset or invalid value becomes `dev` with `debug=True`, otherwise `prod`. ## Feedback Send feedback from your backend, for example from your own support form or a chatbot. There is no UI on the server: `submit` posts what you give it and returns the feedback ID. It shows up under **Feedbacks** in the dashboard. ```python theme={"dark"} from lerix import FeedbackAttachment feedback_id = lerix.feedback.submit( message="The export button does nothing", email="ana@example.com", # optional labels=["bug"], # label keys from the dashboard user_id=user.id, # optional, your own ID for the person custom={"plan": "pro"}, # optional, up to 50 keys / 16 KB of JSON attachments=[FeedbackAttachment(data=png_bytes, width=1280, height=800)], ) ``` * `message` is trimmed and must be 1 to 4,000 characters. * `attachments`: up to 5 PNG, JPEG or WebP images of up to 5 MB each, as `bytes`, `FeedbackAttachment` or a dict with the same fields. The type is read from the bytes; a `content_type` that disagrees is an error. * Inside a [traced request](/frameworks/python/request-tracing), the feedback is linked to the request's trace. * Unlike `track`, `submit` blocks and raises `lerix.LerixApiError`: code `INVALID_FEEDBACK` for bad input (nothing is sent), or the API error after two retries of a network error, 429 or 5xx. Retries reuse the same client ID, so the feedback is never created twice. It returns `None` with `enabled=False`. > [!NOTE] > > The `user_id` you pass is stored as *reported* by your server. Replies by > push only go to users verified through a client SDK's `setUser`. # Python — Installation Source: https://docs.lerix.dev/frameworks/python/installation Add the Lerix Python SDK to your backend to report unhandled exceptions and send push notifications from your server. ## Introduction `lerix` is the server-side Lerix SDK for Python. After one call to `lerix.init()`, every unhandled exception in your service is reported to the dashboard with its stack trace, host, release and request context, and your code gets a typed client for the push notifications REST API. Drop-in hooks are included for FastAPI, Flask and Django. > [!NOTE] > > Create a project with the **Python** framework in the [dashboard](https://app.lerix.dev) > and copy its **API key** and **project id** from **Project Settings**. ## Requirements | | Minimum | | - | - | | Python | 3.9 | The package has no runtime dependencies. The framework hooks import FastAPI, Flask or Django only when you use them. ## 1. Install the package ```bash theme={"dark"} pip install lerix ``` ## 2. Initialise the SDK Call `lerix.init()` once, as early as possible in your program: ```python main.py theme={"dark"} import os import lerix lerix.init( api_key=os.environ["LERIX_API_KEY"], project_id=os.environ["LERIX_PROJECT_ID"], environment=os.environ.get("ENV", "production"), ) ``` With no arguments, `init()` reads `LERIX_API_KEY`, `LERIX_PROJECT_ID` and `LERIX_ENVIRONMENT` from the environment: ```python theme={"dark"} import lerix lerix.init() ``` That is the whole setup. `init()` registers the service in the background and installs `sys.excepthook` and `threading.excepthook`, so uncaught exceptions are reported from now on and flushed before the interpreter exits. See [Error Tracking](/frameworks/python/error-tracking) for what gets captured and how to report errors yourself. ### Add the hook for your framework Web frameworks catch exceptions themselves to return a `500`, so they never reach `sys.excepthook`. Add the matching hook to report them: **FastAPI / Starlette** ```python main.py theme={"dark"} from fastapi import FastAPI import lerix from lerix.integrations.fastapi import LerixMiddleware lerix.init() app = FastAPI() app.add_middleware(LerixMiddleware) ``` **Flask** ```python app.py theme={"dark"} from flask import Flask import lerix from lerix.integrations.flask import LerixFlask lerix.init() app = Flask(__name__) LerixFlask(app) ``` **Django** ```python settings.py theme={"dark"} import lerix lerix.init() MIDDLEWARE = [ "lerix.integrations.django.LerixMiddleware", # ... ] ``` ## 3. Verify the integration Add a route that raises and call it once: ```python theme={"dark"} @app.get("/lerix-test") def lerix_test(): raise RuntimeError("Lerix test error") ``` The client receives the framework's normal `500` response and the error appears in your [dashboard](https://app.lerix.dev) within seconds. Remove the route afterwards. ## Options Every option is a keyword argument of `lerix.init()` (and of `LerixClient`). | Option | Default | Description | | - | - | - | | `api_key` | `LERIX_API_KEY` | Project API key | | `project_id` | `LERIX_PROJECT_ID` | Project id | | `url` | `https://api.lerix.dev/v1` | API base URL (`LERIX_API_URL`) | | `environment` | `LERIX_ENVIRONMENT` or `ENV` | Sent as metadata with every report | | `app` | from `pyproject.toml` | `AppInfo(id, name, version, build_number)` used to identify this service | | `enabled` | `True` | `False` turns the SDK into a no-op, useful in tests | | `debug` | `False` | Log requests and reports on the `lerix` logger | | `capture_unhandled` | `True` | Install `sys.excepthook` / `threading.excepthook` | | `state_path` | `.lerix/state.json` | Where the anonymous user id is persisted; `False` keeps it in memory | | `user_id` | | Pin the anonymous user id instead of registering one | | `flush_timeout` | `2.0` | Seconds `flush()` and shutdown wait for pending reports | | `timeout` | `10.0` | HTTP timeout per request, in seconds | | `cwd` | process cwd | Directory used to find `pyproject.toml` and the state file | ## How your service appears in the dashboard The SDK registers your service as a `server` app. The app id defaults to the `name` in `pyproject.toml`'s `[project]` table and the release to its `version`, so reports are grouped per service and per release. Override them with the `app` option when you deploy several instances of the same code base or want a git SHA as the build number: ```python theme={"dark"} import os import lerix from lerix import AppInfo lerix.init( app=AppInfo(id="orders-api", version=os.environ.get("APP_VERSION"), build_number=os.environ.get("GIT_SHA")), ) ``` An anonymous user is registered on first boot and stored in `.lerix/state.json` under the working directory, so restarts reuse it. Add that folder to `.gitignore`, or mount it in containers. Pass `state_path=False` to skip persistence, or `user_id` to control the id yourself. ## Next steps > **[Error Tracking](/frameworks/python/error-tracking)** > > What the hooks report, how to change the policy, and manual reporting. > **[Push Notifications](/frameworks/python/push-notifications)** > > Send, schedule, update and unsend notifications from your backend. # Python — Push Notifications Source: https://docs.lerix.dev/frameworks/python/push-notifications Send, schedule, update and unsend push notifications from your Python backend with a typed client for the Lerix REST API. ## Introduction A backend does not receive push notifications, it sends them. The Python SDK wraps the [push notifications REST API](/api-reference/push-notifications) in a typed client so your services can notify the users of your mobile and web apps without hand-writing HTTP calls. ## 1. Create a private key Notifications are sent to the devices of the project your **app** uses, which is usually a different project from the one your backend reports errors to. In that project's **Project Settings**, create a **private key** with the *send push notifications* permission. > [!WARNING] > > A private key can send notifications to all of a project's users. Keep it > in an environment variable on the server and never ship it to a client. ## 2. Create the client Pass the target project id and the private key to `LerixNotifications`: ```python notifications.py theme={"dark"} import os from lerix import LerixNotifications push = LerixNotifications( project_id=os.environ["MOBILE_PROJECT_ID"], api_key=os.environ["MOBILE_PROJECT_PRIVATE_KEY"], ) ``` With no arguments it reads `LERIX_NOTIFICATIONS_PROJECT_ID` and `LERIX_NOTIFICATIONS_API_KEY` from the environment. The client is independent of `lerix.init()`, so you can use it in a service that does not report errors at all. ## 3. Send a notification ```python orders.py theme={"dark"} result = push.send( title="Order shipped", body="Your order #1234 is on its way", device_tokens=[user.lerix_device_id], metadata={"orderId": "1234"}, ) notification_id = result.notification_id ``` `device_tokens` are the Lerix device ids your client apps obtain from `getDeviceId()` and send to your backend at login. Save the returned `notification_id`, every other method takes it. ### Broadcast to a topic ```python theme={"dark"} push.send( title="Weekend sale", body="20% off everything until Sunday", send_by_topic=True, topic="promotions", ) ``` ### Schedule for later ```python theme={"dark"} from datetime import timedelta push.send( title="Reminder", body="Your appointment is in one hour", device_tokens=[device_id], sent_at=appointment - timedelta(hours=1), ) ``` ### Parameters `send()` accepts these as keyword arguments, or as a `SendNotificationInput` dataclass. | Parameter | Type | Required | Description | | - | - | - | - | | `title` | `str` | Yes | Notification title | | `body` | `str` | Yes | Notification body | | `device_tokens` | `list[str]` | Unless `send_by_topic` | Lerix device ids to deliver to | | `send_by_topic` | `bool` | No | Deliver to every device subscribed to `topic` instead | | `topic` | `str` | When `send_by_topic` | Topic key | | `sound` | `str` | No | Custom sound file bundled in the client app | | `image_url` | `str` | No | Public `https://` image shown as a rich image | | `metadata` | `dict` | No | Delivered to the client as `payload.metadata` | | `sent_at` | `str \| datetime` | No | ISO 8601 time or `datetime` to schedule delivery | Platform notes for `sound` and `image_url` are in the [REST API reference](/api-reference/push-notifications). ## 4. Manage a sent notification ```python theme={"dark"} # Change the content. Scheduled ones are edited in place; sent ones are redelivered. push.update(notification_id, title="Delivered", body="Enjoy your order!") # Remove an already-delivered notification from devices. push.unsend(notification_id) # Stop a scheduled notification before it goes out. push.cancel(notification_id) # Per-recipient delivery outcome as reported by APNs, FCM or the browser push service. deliveries = push.deliveries(notification_id) ``` | Method | REST endpoint | | - | - | | `send(...)` | [`POST /notifications/send`](/api-reference/push-notifications) | | `update(id, title, body)` | [`POST /notifications/{id}/update`](/api-reference/update-notification) | | `unsend(id)` | [`POST /notifications/{id}/unsend`](/api-reference/unsend-notification) | | `cancel(id)` | [`POST /notifications/{id}/cancel`](/api-reference/cancel-notification) | | `deliveries(id)` | [`GET /notifications/{id}/deliveries`](/api-reference/notification-deliveries) | ## Errors Every method raises `LerixApiError` when the API rejects the request. `code` is the backend's error code and `status` the HTTP status: ```python theme={"dark"} from lerix import LerixApiError try: push.cancel(notification_id) except LerixApiError as error: if error.code == "CANNOT_CANCEL_SENT_NOTIFICATION": ... # already delivered ``` Invalid input such as a missing `topic` with `send_by_topic` is rejected locally with code `INVALID_INPUT` before any request is made. The full list of codes is in [Error & success codes](/developers/errors-responses). ## Several target projects Create one `LerixNotifications` per app when one backend serves more than one: ```python theme={"dark"} rider = LerixNotifications(project_id="rider-app", api_key=os.environ["RIDER_PRIVATE_KEY"]) driver = LerixNotifications(project_id="driver-app", api_key=os.environ["DRIVER_PRIVATE_KEY"]) ``` # Python — Request Tracing Source: https://docs.lerix.dev/frameworks/python/request-tracing One span per request in FastAPI, Flask and Django, psycopg and httpx/requests spans, sampling, and the trace behind every error. ## Introduction After the [installation](/frameworks/python/installation), turn on request tracing to see, under **Requests** in the dashboard, every request your service handled with its route, duration, queries and outbound calls — and to open the exact request behind any error. See [Request Tracing](/modules/request-tracing) for what the dashboard shows. ## Enable it ```python theme={"dark"} import lerix lerix.init(api_key="...", project_id="...", tracing={"enabled": True}) ``` Tracing is off by default; when off, the `lerix.tracing` package is never imported and no hook is installed. The request span itself comes from the framework integration you already registered: | Framework | Integration | Route template | | - | - | - | | FastAPI / Starlette | `LerixMiddleware` | `GET /users/{user_id}` | | Flask | `LerixFlask(app)` | `GET /items/` | | Django | `lerix.integrations.django.LerixMiddleware` | `GET /things//` | ## What is recorded * **One server span per request**, named after the route template, never the raw path. An incoming W3C `traceparent` header is continued. * **PostgreSQL** — `psycopg` (3) and `psycopg2`, so Django and SQLAlchemy on Postgres too. Each query becomes a `client` span named after the SQL template (`SELECT * FROM users WHERE id = ?`); parameters and rows are never recorded. * **Outbound HTTP** — `httpx` (sync and async) and `requests`. Each call becomes a `client` span named `METHOD host` and carries a `traceparent` header so a downstream service continues the trace. Calls to the Lerix API itself are ignored. Each hook can be switched off: ```python theme={"dark"} lerix.init(tracing={"enabled": True, "instrumentations": {"psycopg": True, "httpx": True, "requests": False}}) ``` Anything else can be traced by hand: ```python theme={"dark"} tracer = lerix.get_client().tracing.tracer with tracer.child("cache get", "client", {"server.address": "redis"}): cache.get(key) ``` The block ends the span with `error` if it raises. Outside a traced request `tracer.child(...)` does nothing. ## The current trace ```python theme={"dark"} ctx = lerix.current() log.info("handling order, trace %s", ctx.trace_id if ctx else None) ``` The context follows `await`s and task switches (it is a `contextvars` variable); a thread you start by hand does not inherit it. ## Sampling | Option | Default | Description | | - | - | - | | `sample_rate` | `0.1` | Share of successful requests kept in full. Decided once at the root and inherited downstream. | | `slow_threshold_ms` | `2000` | A request slower than this is always kept in full. | | `max_spans_per_request` | `1000` | Child spans held per request until it ends. | Failed requests and requests that reported an error are always kept whole. Every request's root span is still sent, so the per-route counts and percentiles in the dashboard cover 100% of traffic. ## Errors and traces Every error report made inside a request — the integrations' automatic reports as well as your own `lerix.capture_exception()` — carries that request's trace, so the issue page shows the waterfall inline with the failing query highlighted. An error reported outside any request records as before, with no trace attached. ## Client addresses Each request span carries the client's address, resolved the way the project asks under **Project settings → Tracing**: in the default `masked` mode the SDK zeroes the last IPv4 octet or truncates IPv6 to its /64 before anything is sent; in `hashed` and `full` modes the address is handled on the Lerix side. The trusted-proxy depth and header decide how it is read from `X-Forwarded-For`; the whole chain is never trusted. ## Exporter options | Option | Default | Description | | - | - | - | | `flush_interval` | `5.0` | Seconds between exports when the batch is not full | | `max_queue_size` | `2048` | Spans held in memory before new ones are dropped | | `max_batch_size` | `512` | Spans per export request | | `request_timeout` | `10.0` | Timeout of one export request, in seconds | Spans leave as OTLP/HTTP JSON from a background thread. If the Lerix API is unreachable the batch is counted and dropped; nothing is retried, nothing blocks a request, nothing raises into your code. Counters are on `lerix.get_client().tracing.stats()`. # React — Error Tracking Source: https://docs.lerix.dev/frameworks/react/error-tracking Report errors and automatically capture uncaught exceptions from your React app to the Lerix dashboard. ## Introduction Lerix captures errors from your React app so you can debug issues faster. After you complete the [React installation](/frameworks/react/installation), use `useLerix().throwError()` to send custom errors to your dashboard — uncaught errors are captured automatically, with no extra code required. > [!NOTE] > > Make sure your app is wrapped in `LerixProvider` before reporting errors. ## Report an error ```tsx theme={"dark"} import { useLerix, BugSeverity } from '@lerix-dev/lerix-react'; function MyComponent() { const lerix = useLerix(); const onSomethingFailed = async () => { try { await riskyOperation(); } catch (error) { lerix.throwError( String(error), undefined, undefined, BugSeverity.HIGH, { userId: '123', action: 'payment', amount: 100 }, ); } }; } ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `issue` | `string` | Yes | Error message or description | | `stack` | `string[]` | No | Stack trace frames. If omitted, one is captured automatically from the call site — see [Automatic stack capture](#automatic-stack-capture) below | | `type` | `BugType` | No | Bug classification (default: `RUNTIME_ERROR`) | | `severity` | `BugSeverity` | No | Severity level (default: `MEDIUM`) | | `metadata` | `Record` | No | Additional context to attach to the report | ### `BugType` values | Value | Description | | - | - | | `RUNTIME_ERROR` | An unhandled exception thrown at runtime | | `LOGIC_BUG` | Incorrect behavior that doesn't throw | | `UI_BUG` | A visual or layout defect | | `NETWORK_ERROR` | A failed or malformed network request | | `PERFORMANCE` | Slowness, jank, or excessive resource use | | `COMPATIBILITY` | Breaks on a specific browser or device | | `VALIDATION_ERROR` | Invalid input passed validation it shouldn't have | | `SECURITY` | A security-relevant defect | | `CRASH` | The app terminated unexpectedly | | `UNKNOWN` | None of the above | ### `BugSeverity` values | Value | Description | | - | - | | `CRITICAL` | Blocks core functionality or affects all users | | `HIGH` | Serious impact, but a workaround exists | | `MEDIUM` | Noticeable but limited impact | | `LOW` | Minor or cosmetic | | `UNKNOWN` | Severity not yet assessed | ## Automatic error capture `LerixProvider` installs error capture two ways, so nothing needs wrapping in a `try`/`catch` for baseline coverage: * `LerixErrorBoundary` catches errors thrown during rendering — React's error boundary contract only covers the render phase, not event handlers or async code. * Global `window.onerror` and `unhandledrejection` listeners catch everything else — event handlers, timers, promise rejections. Unlike a native mobile crash handler, a browser tab isn't about to terminate when one of these fires, so the report is sent immediately rather than persisted for the next launch. ```tsx theme={"dark"} import { LerixErrorBoundary } from '@lerix-dev/lerix-react'; Something went wrong.

}>
``` `fallback` can be a plain element, or a function receiving the caught error: `fallback={(error) =>

{String(error)}

}`. To disable the global handlers (e.g. if your app already has its own error reporting and you only want manual `throwError` calls): ```tsx theme={"dark"} ``` `LerixErrorBoundary` still catches render errors regardless of this flag — it's a separate, explicit component you control by where you place it. ## Automatic stack capture When you call `throwError()` without a `stack` argument, one is captured automatically from the call site — you only need to pass one explicitly when reporting a caught `Error`'s own stack instead (e.g. `error.stack?.split('\n')`). Either way, every stack frame is resolved from its compiled/bundled position (e.g. a bundler's output file and line) back to the original source file and line, using the build's own source map — the same mechanism browser DevTools use to show you readable stack traces instead of minified bundle positions. This happens automatically; there's nothing to configure. > [!NOTE] > > Resolution requires a source map to be reachable at the frame's URL — > always true in development, and true in production only if your build > serves its `.map` files publicly. If your production build keeps source > maps private (a common choice), frames fall back to their compiled > position rather than failing to report — resolution is best-effort and > never blocks or delays the report itself. > [!TIP] > > To get readable production stack traces while keeping your source maps > private, upload them to Lerix after each build. See > [React source maps](/frameworks/react/source-maps). ### Verify the integration ```tsx theme={"dark"} lerix.throwError('Test error'); ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds, with a resolved stack trace pointing at this exact line. ## Filtering errors Requires `@lerix-dev/lerix-react` 1.2.0 or later. Two optional `LerixProvider` options decide which errors are sent. They apply to every report: `LerixErrorBoundary`, the global handlers and `throwError()`. ```tsx theme={"dark"} (event.message.includes('extension://') ? null : event), }} > ``` * `ignoreErrors` runs first. A string drops any error whose message contains it, ignoring case. A `RegExp` drops any error whose message it matches. Empty strings are skipped. * `beforeSend` then sees only what's left. Return the event (changed or not) to send it, or `null` to drop it. If it throws, the original event is sent and the exception never reaches your app. * The message you match against is the issue title, which includes the error class, for example `TypeError: boom`. `beforeSend` receives a `LerixErrorEvent`: `{ message, stack, type, severity, metadata? }`. `stack` is an array of frames, already resolved through your source maps. Lerix also groups repeated errors into one issue and filters common noise on the server, for every SDK version. See [How errors are grouped into issues](/modules/error-tracking#how-errors-are-grouped-into-issues). # React — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/react/feedback-analytics Add an in-app feedback dialog, an NPS survey and product analytics to your React app with hooks. Requires `@lerix-dev/lerix-react` 1.3.0 or later (with `@lerix-dev/lerix-core` 1.4.0). For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). ## Setup Nothing extra is needed beyond the `LerixProvider` from [Installation](/frameworks/react/installation). Analytics starts on mount, and URL changes are recorded as screen views through the History API. The provider's `options` accept these settings for the new modules: ```tsx main.tsx theme={"dark"} ``` | Option | Description | | - | - | | `environment` | Sent with feedback, survey responses and events | | `locale` | Language of the feedback and NPS UI, such as `ar` or `en-US` | | `analytics` | `enabled`, `autoSessions`, `autoScreens`, all `true` by default | | `feedback` | `theme` (`auto`, `light`, `dark`), `primaryColor`, `strings` per language, `zIndex` | | `captureConsoleErrors` | Records `console.error` calls as breadcrumbs. Off by default. | | `release` | Also used for `$app_update`: the event fires when this value changes | ## In-app feedback ```tsx theme={"dark"} import { useLerixFeedback } from '@lerix-dev/lerix-react'; function FeedbackButton() { const feedback = useLerixFeedback(); return ; } ``` The dialog (message, labels, screenshot with pen annotation, email, review with **Show details**) renders inside a Shadow DOM, so your CSS can't reach it and it can't break your layout. It follows light or dark mode (`.dark` or `[data-theme]` on ``, else the OS) and ships English and Arabic (right to left). The dialog and the screenshot library load on first use, in their own chunk. `useLerixFeedback()` returns `{ show, submit, setUserEmail, isOpen }`. `show(options?)` resolves `{ submitted, id?, clientId? }` and takes: ```tsx theme={"dark"} await feedback.show({ labels: ['bug', 'improvement'], // only these keys, in this order email: 'required', // 'hidden' | 'optional' | 'required' screenshot: 'optional', // 'hidden' | 'optional' prefillEmail: user.email, custom: { plan: 'pro' }, // up to 50 keys / 16 KB }); ``` ### Hide sensitive content `LerixConfidential` adds `data-lerix-mask`, which blacks the element out of screenshots. You can also put the attribute on any element yourself. ```tsx theme={"dark"} import { LerixConfidential } from '@lerix-dev/lerix-react'; {user.email} ... ``` The dialog also offers **pixel-perfect capture**, which uses the browser's screen-share prompt to capture canvas, video and cross-origin content exactly. ### Your own form ```tsx theme={"dark"} const { submit } = useLerixFeedback(); const result = await submit({ message: 'Checkout button does nothing', labels: ['bug'], email: 'user@example.com', screenshots: [blob], // PNG, JPEG or WebP Blobs, up to 5 MB each }); // result: { clientId, id?, status: 'created' | 'duplicate' | 'queued' } ``` Feedback is queued in IndexedDB and retried until the server accepts it. ## NPS survey ```tsx theme={"dark"} import { useLerixSurvey } from '@lerix-dev/lerix-react'; function Home() { useLerixSurvey({ autoShow: true }); // checks once, 2 seconds after mount return ; } ``` Or call it yourself at the right moment: ```tsx theme={"dark"} const { maybeShowNps } = useLerixSurvey(); await maybeShowNps(); // only when this install is due await maybeShowNps({ force: true }); // always (testing) ``` `useLerixSurvey({ autoShow: true, delayMs: 5000 })` changes the delay. The SDK shows the card only when this install is due (see [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown)). ## Analytics ```tsx theme={"dark"} import { useLerixScreen, useLerixTrack } from '@lerix-dev/lerix-react'; function Checkout() { const track = useLerixTrack(); useLerixScreen('Checkout'); // names a screen the URL doesn't, such as a modal step return ; } ``` `$first_open`, `$session_start` / `$session_end` (per tab, a new session after 30 minutes of inactivity), `$app_update` and `$screen_view` (from the URL) are recorded automatically. `track` never throws. The core facade is re-exported for code outside components: ```ts theme={"dark"} import { Lerix } from '@lerix-dev/lerix-react'; Lerix.track('export_finished', { rows: 1200 }); await Lerix.analytics.flush(); Lerix.analytics.setEnabled(false); // opt out: persisted, clears the queue ``` Events are batched and sent with `fetch(..., { keepalive: true })` when the tab is hidden, so the last events of a visit aren't lost. > [!NOTE] > > Call [`setUser`](/modules/users-identity) after sign-in. Analytics then > counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs The last 50 breadcrumbs are attached to feedback. Navigation is added automatically. ```ts theme={"dark"} Lerix.addBreadcrumb('Applied coupon', 'ui', { code: 'EID' }); ``` Categories: `navigation`, `log`, `http`, `ui`, `custom`. # React — Installation Source: https://docs.lerix.dev/frameworks/react/installation Add Lerix to your React app in a few steps. ## Requirements * React 18+ > [!NOTE] > > This is a dedicated React library — a `LerixProvider` component, a > `LerixErrorBoundary`, and `useLerix()`/`useLerixNotifications()` hooks — > built on top of `@lerix-dev/lerix-core`, the framework-agnostic engine shared by > every Lerix web SDK. Building an Angular app instead? See the > [Angular SDK](/frameworks/angular/installation). If you're using > neither React nor Angular, `@lerix-dev/lerix-core` also works directly. ## 1. Add the dependency ```bash theme={"dark"} npm install @lerix-dev/lerix-react ``` ## 2. Register the SDK Wrap your app in `LerixProvider`, and `LerixErrorBoundary` to catch render errors: ```tsx main.tsx theme={"dark"} import { createRoot } from 'react-dom/client'; import { LerixErrorBoundary, LerixProvider } from '@lerix-dev/lerix-react'; import App from './App'; createRoot(document.getElementById('root')!).render( Something went wrong.

}>
, ); ``` > [!NOTE] > > Find your API key and project ID in the [Lerix Dashboard](https://app.lerix.dev) under **Project Settings**. Registration with the backend, and the automatic crash handler, run once at mount — safe even under React 18 StrictMode's double-effect-invocation in development, since initialization is re-entrant-safe. ### Parameters `LerixProvider`'s `options` prop: | Parameter | Type | Required | Description | | - | - | - | - | | `apiKey` | `string` | Yes | Your project API key from the dashboard | | `projectId` | `string` | Yes | Your project ID (slug) from the dashboard | | `url` | `string` | No | Backend base URL. Defaults to `https://api.lerix.dev/v1` — only change this if you're pointing at a self-hosted backend | | `release` | `string` | No | The version this build ships as (e.g. `1.4.0` or a commit SHA). Pass the same value when uploading source maps so production stack traces show your original files: see [Source maps](/frameworks/react/source-maps) | | `debugMode` | `boolean` | No | Print SDK request/response logs to the console (default: `false`) | | `enableCrashReporting` | `boolean` | No | Install the automatic crash handler (default: `true`) — see [Error Tracking](/frameworks/react/error-tracking) | | `onError` | `(error: unknown) => void` | No | Called if initialization itself fails (e.g. network unreachable) | ## 3. Verify the integration Use `useLerix()` and throw a test error to confirm everything is working: ```tsx theme={"dark"} import { useLerix } from '@lerix-dev/lerix-react'; function MyComponent() { const lerix = useLerix(); return ; } ``` Then open your [dashboard](https://app.lerix.dev) — the error should appear within seconds. > [!TIP] > > You're all set! Continue to [Error Tracking](/frameworks/react/error-tracking) to report custom errors and enable automatic capture, or [Push Notifications](/frameworks/react/push-notifications) to enable push delivery. # React — Push Notifications Source: https://docs.lerix.dev/frameworks/react/push-notifications Configure standard Web Push (VAPID) delivery for your React app — no Firebase, no OneSignal required. ## Introduction Lerix delivers push notifications to React apps using the standard **Web Push protocol (VAPID)** — the same open standard Chrome, Firefox, Edge, and modern Safari all implement natively. No Firebase project, no OneSignal account — Lerix generates and manages the key pair for you. After you complete the [React installation](/frameworks/react/installation), follow the steps below to enable push delivery. > [!NOTE] > > Make sure your app is wrapped in `LerixProvider` before configuring push notifications. ## 1. Copy the service worker Web Push requires a service worker file served from your app's own origin — a package can't add this file to your app automatically. Copy it into your `public` folder so it's reachable at your site's root: ```bash theme={"dark"} cp node_modules/@lerix-dev/lerix-core/sw/lerix-sw.js your-app/public/lerix-sw.js ``` ## 2. Generate a Web Push key From the dashboard: **Notifications → Settings → Web Push → Generate keys**. This is a one-click step — Lerix generates and stores the VAPID key pair for the project; there's nothing to copy into your code. ## 3. Subscribe ```tsx theme={"dark"} import { useEffect } from 'react'; import { useLerixNotifications } from '@lerix-dev/lerix-react'; function MyComponent() { const notifications = useLerixNotifications(); useEffect(() => { notifications.setOnNotificationReceived((payload) => { // Fired while a tab is open. console.log(payload.title, payload.body); }); notifications.setOnNotificationTapped((payload) => { // Fired when the user clicks the notification — including one that // opened the page from a fully closed tab. const screen = payload.metadata['screen']; }); }, []); const enablePush = async () => { const subscribed = await notifications.subscribe(); }; return ; } ``` `subscribe()` prompts for notification permission, registers the service worker, subscribes this browser to push, and registers the subscription with the backend — one call for the whole flow. ## The notification payload Both callbacks receive a `LerixNotificationPayload`: ```ts theme={"dark"} interface LerixNotificationPayload { notificationId: string | null; title: string | null; body: string | null; metadata: Record; imageUrl: string | null; } ``` ## Device identifiers | Method | Returns | Use it for | | - | - | - | | `getRegisteredTokenId()` | Lerix's own id for this registered device | The value for the dashboard's/REST API's device-targeting field | | `getDeviceToken()` | The raw `PushSubscription`, stringified | Rarely needed directly; diagnostics only | | `getDeviceId()` | A locally generated identifier, unrelated to Lerix's backend | Local diagnostics only; **not** what the dashboard's send flow expects | ```tsx theme={"dark"} const tokenId = notifications.getRegisteredTokenId(); ``` All three return `null` until `subscribe()` has completed at least once. ## Sending metadata Attach arbitrary custom data with `metadata` (a JSON object) when sending — from the dashboard's **Send notification** tab, or the REST API's [`metadata` parameter](/api-reference/push-notifications). It's delivered back to the page inside `payload.metadata`: ```json theme={"dark"} { "title": "New message from John", "body": "Hey, are you free later?", "metadata": { "chatId": "456", "screen": "chat" } } ``` ```tsx theme={"dark"} notifications.setOnNotificationTapped((payload) => { const chatId = payload.metadata['chatId']; }); ``` ## Sending an image Set `imageUrl` when sending — from the dashboard's **Send notification** tab, or the REST API's [`imageUrl` parameter](/api-reference/push-notifications). This works automatically with no extra setup: the service worker renders it directly as the notification's icon/image. > [!NOTE] > > **Custom sound is not supported for web push.** The Web Notifications API > doesn't expose a way to set a custom notification sound across browsers, > so the dashboard/API's `sound` parameter is ignored for browser > recipients — it only applies to iOS and Android. ## Target a user on every device If your product also ships on other platforms (web, mobile or desktop), add them to the same project and link each install to your own user ID after login. Your backend can then send to `externalUserIds` and reach that person on every device in one request. ```tsx theme={"dark"} import { useLerix } from '@lerix-dev/lerix-react'; const lerix = useLerix(); // inside a component await lerix.setUser('user_123'); // after login await lerix.clearUser(); // on logout ``` See [Identify users](/modules/users-identity) for identity verification and a sending example. ## Topics Instead of targeting specific device tokens, a device can subscribe to a named topic — send one notification to the topic and every subscriber receives it. A topic doesn't need to be created ahead of time; it's created automatically the first time any device subscribes to it. You can also view, create, and delete topics from the **Notifications → Topics** tab in the dashboard. ```tsx theme={"dark"} await notifications.subscribeToTopic('promotions'); await notifications.unsubscribeFromTopic('promotions'); ``` To send to a topic, use the dashboard's **Send notification** tab (select "Topic" as the audience), or call the [REST API](/api-reference/push-notifications) with `sendByTopic: true`. ## Browser support Chrome, Firefox, and Edge support Web Push fully. Safari requires macOS 13+ or iOS 16.4+ — older Safari doesn't support the standard at all. # Release control in React Source: https://docs.lerix.dev/frameworks/react/release-control Ask users to reload for a new deployment, require it, or show a maintenance screen in your React app, or build your own with a hook. For what release control does and how to set rules in the dashboard, read [Release control](/modules/release-control/overview). ## Setup Release control is on by default inside the `LerixProvider` from [Installation](/frameworks/react/installation), with the built-in screens. Set `release` so update rules can compare versions: ```tsx main.tsx theme={"dark"} ``` | Option | Default | Description | | - | - | - | | `release` | none | The version this deployment ships as. Without it, update rules never apply on the web. Use a version with digits, such as `1.4.0`, not a commit SHA. | | `appControl.enabled` | `true` | Checks for updates and maintenance. `false` turns the module off. | | `appControl.ui` | `true` | Shows the built-in screens. `false` leaves the drawing to you. | ## What users see * **Maintenance**: a fixed overlay above everything on the page, with **Try again**. The rest of the page can't be clicked or focused. * **Required update**: the page reloads by itself to load the newest deployment, at most once every 5 minutes per tab. If it is still out of date, a full-screen message stays with a **Reload** button. * **Optional or recommended**: a bar at the bottom of the page with **Update** (reloads), **Later** and, when allowed, **Skip this version**. Web pages don't receive silent push: the SDK checks on load, when the tab is visible again, and every minute during an emergency. See [Delivery and offline behaviour](/modules/release-control/delivery-offline). ## Your own screens Set `appControl: { ui: false }` and use `useLerixAppControl()`. It returns the whole `state`, its parts `update`, `maintenance` and `sdk` (each `null` when nothing applies, fields in [The state](/modules/release-control/custom-ui#the-state)), and the helpers. The component re-renders when the state changes. ```tsx release-gate.tsx theme={"dark"} import { useLerixAppControl } from '@lerix-dev/lerix-react'; export function ReleaseGate({ children }: { children: React.ReactNode }) { const { update, maintenance, refresh, openStore, later, skip } = useLerixAppControl(); if (maintenance) { return ( refresh()} /> ); } if (update?.state === 'required') { return openStore()} />; } return ( <> {update && ( openStore()} onLater={() => later()} onSkip={update.allowSkip ? () => skip() : undefined} /> )} {children} ); } ``` | Returned | What it is | | - | - | | `state` | `{ update, maintenance, sdk }` | | `update`, `maintenance`, `sdk` | The same parts, for convenience | | `refresh()` | Checks now and resolves the new state | | `openStore()` | Reloads the page | | `later()` | Hides the soft prompt for `remindAfterHours` | | `skip()` | Hides the soft prompt until a newer version (optional updates only) | With `ui: false` there is no automatic reload for a required update: call `openStore()` from your own button. # React — Source Maps Source: https://docs.lerix.dev/frameworks/react/source-maps Upload your production source maps privately so Lerix shows React errors at the original file and line. ## Introduction Production React builds are minified, so a stack trace from a real user looks like this: ```text theme={"dark"} onConnMessage@https://app.example.com/assets/index-Cr8GYWvZ.js:10:14780 ``` When you upload your build's source maps to Lerix, the dashboard maps each frame back to your original code, for example `src/components/Chat.tsx:142`, and shows the surrounding lines. The maps are uploaded privately and deleted from your build output before you deploy. They are never served publicly, so your source code stays private. > [!NOTE] > > Complete the [React installation](/frameworks/react/installation) first. > Setting a release requires `@lerix-dev/lerix-core` 1.1.0 or newer. ## 1. Enable source maps **Vite** Generate hidden source maps in `vite.config`: ```ts vite.config.ts theme={"dark"} import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { sourcemap: 'hidden', }, }); ``` `'hidden'` generates the `.map` files without adding a `//# sourceMappingURL` comment to the shipped JavaScript, so browsers never try to load them. The output folder is `dist`. **Create React App** Create React App generates source maps by default, as long as you haven't set `GENERATE_SOURCEMAP=false`. The output folder is `build`. Run your production build and check that `.js.map` files appear next to the bundles. ## 2. Create a private API key Uploading source maps needs a private key, separate from the public project key your app uses. 1. Open your project in the [Lerix Dashboard](https://app.lerix.dev). 2. Go to **Project settings** → **API keys**. 3. Create a key with the **Upload source maps** permission (or **Full access**). > [!WARNING] > > This key is secret. Store it as a CI secret and never put it in client code > or commit it to your repository. ## 3. Upload the maps after each build After the build and before you deploy, run the Lerix CLI on the build output: ```bash theme={"dark"} npx @lerix-dev/lerix-cli upload ``` Run it from your project folder. It finds the build output on its own (`dist` (Vite) or `build` (Create React App)) and uses the `version` from `package.json` as the release. To set them yourself: `npx @lerix-dev/lerix-cli upload dist --release 1.4.0`. The CLI uploads every `*.js.map` file in the folder (it skips `node_modules` and CSS maps), then deletes the `.map` files from the build output so they are never deployed. If any upload fails, it keeps the maps and exits with code 1, so you can retry the step. The CLI requires Node.js 18.17 or newer. ### Upload on every build Instead of running the command yourself, install the CLI in your project and let npm run it after each build: ```bash theme={"dark"} npm install --save-dev @lerix-dev/lerix-cli ``` ```json package.json theme={"dark"} "scripts": { "build": "vite build", "postbuild": "lerix upload --optional" } ``` Now every `npm run build` uploads the maps. `--optional` keeps local builds working: on a machine without `LERIX_SECRET_KEY` (a developer's laptop), the upload is skipped instead of failing the build, and the `.map` files are still deleted. Set the keys in your CI or build server, where the release build runs. > [!NOTE] > > pnpm and Yarn 2+ don't run `postbuild` scripts. Chain the command instead: > `"build": "vite build && lerix upload --optional"`. ### Options Each option can also be set with an environment variable. | Option | Environment variable | Description | | - | - | - | | `--release` | `LERIX_RELEASE` | The version this build ships as. Defaults to `version` in `package.json`. Pass the same value to the SDK (see step 4) | | `--api-key` | `LERIX_API_KEY` | Your normal public project API key | | `--project-id` | `LERIX_PROJECT_ID` | Your project ID | | `--secret-key` | `LERIX_SECRET_KEY` | The private key from step 2 | | `--url` | `LERIX_URL` | Lerix API URL. Defaults to `https://api.lerix.dev/v1` | | `--keep-maps` | | Keep the `.map` files in the build output instead of deleting them | | `--dry-run` | | List the maps that would be uploaded, without uploading anything | | `--optional` | | Skip the upload instead of failing when the keys aren't set (local builds). The `.map` files are still deleted | ## 4. Pass the same release to the SDK Set `release` in the `LerixProvider` options to the value you passed to `--release`: ```tsx theme={"dark"} import { LerixProvider } from '@lerix-dev/lerix-react'; ``` Lerix matches maps by the script's file name. Bundler chunk names are content-hashed, so they are unique per build, and Lerix prefers maps from the same release. Matching still works if you omit `release`, but setting it is recommended. The release also replaces the `0.0.0` app version previously shown for web apps. > [!TIP] > > Use the same value in both places without editing it by hand: your git > commit SHA or the `version` from `package.json` both work well. ## Example: GitHub Actions This workflow builds a Vite app, uploads the maps, then deploys. Add `LERIX_API_KEY`, `LERIX_PROJECT_ID` and `LERIX_SECRET_KEY` as repository secrets. ```yaml .github/workflows/deploy.yml theme={"dark"} jobs: deploy: runs-on: ubuntu-latest env: LERIX_RELEASE: ${{ github.sha }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci # Make the same release available to the app at build time - run: npm run build env: VITE_LERIX_RELEASE: ${{ github.sha }} - run: npx @lerix-dev/lerix-cli upload env: LERIX_API_KEY: ${{ secrets.LERIX_API_KEY }} LERIX_PROJECT_ID: ${{ secrets.LERIX_PROJECT_ID }} LERIX_SECRET_KEY: ${{ secrets.LERIX_SECRET_KEY }} # Deploy dist here. It no longer contains .map files. ``` Then read the value in your app: ```tsx theme={"dark"} ``` With Create React App, name the variable `REACT_APP_LERIX_RELEASE` and read it from `process.env.REACT_APP_LERIX_RELEASE` instead. ## What you see in the dashboard New errors show the original file, line and function, with the surrounding code. Click **Show minified** to see the stack exactly as the browser sent it. The AI assistant, GitHub issues and the MCP server all receive the readable stack too. * Only errors reported after the maps are uploaded are mapped. Earlier errors keep their minified stack. * Lerix keeps the newest 5,000 maps per project. * Each map can be up to 50 MB. ## Troubleshooting | Problem | Fix | | - | - | | `no .js.map files found` | Source maps are not enabled for the production build. Check step 1 and that you point the CLI at the right output folder. | | `Uploading source maps needs a private API key...` (`SOURCE_MAP_SECRET_KEY_MISSING`) | `--secret-key` is missing or invalid. Create a private key as described in step 2. | | `PROJECT_API_KEY_DONT_HAVE_PERMISSION` | The private key lacks the **Upload source maps** permission. Edit the key or create a new one with that permission. | | The stack is still minified | Check that the SDK `release` matches `--release`, that you uploaded maps from this exact build, and that the error is new. Every rebuild changes chunk hashes, so upload the maps again after each build. | # Web — Feedback, NPS and Analytics Source: https://docs.lerix.dev/frameworks/web/feedback-analytics Add an in-app feedback dialog, an NPS survey and product analytics to any web app with the framework-agnostic core SDK. For plain TypeScript or JavaScript, Vue, Svelte or any web app without a dedicated Lerix package, use `@lerix-dev/lerix-core` 1.4.0 or later. It is the engine behind the React, Next.js and Angular SDKs, and it also reports errors. For what each module does and what you see in the dashboard, read [Feedback](/modules/feedback), [NPS surveys](/modules/nps-surveys) and [Analytics](/modules/analytics). > [!NOTE] > > `@lerix-dev/lerix-js`, the package on the [Web push](/frameworks/web/push-notifications) > page, covers push notifications only. Feedback, NPS and analytics live in > `@lerix-dev/lerix-core`. Using React, Next.js or Angular? Use > [React](/frameworks/react/feedback-analytics), > [Next.js](/frameworks/nextjs/feedback-analytics) or > [Angular](/frameworks/angular/feedback-analytics) instead. ## Setup ```bash theme={"dark"} npm install @lerix-dev/lerix-core ``` ```ts main.ts theme={"dark"} import { Lerix } from '@lerix-dev/lerix-core'; await Lerix.initialize({ apiKey: 'your-api-key', projectId: 'your-project-id', release: '1.4.0', // also drives $app_update environment: 'staging', // default: dev on localhost / *.local or in debug mode, else prod locale: 'ar', // UI language; defaults to the browser's analytics: { autoSessions: true, autoScreens: true }, feedback: { theme: 'auto', primaryColor: '#6E56CF' }, }); ``` | Option | Description | | - | - | | `environment` | Sent with feedback, survey responses and events. Lowercase, 2 to 32 characters. | | `locale` | Language of the feedback and NPS UI, such as `ar` or `en-US` | | `analytics` | `enabled`, `autoSessions`, `autoScreens`, all `true` by default. `enabled: false` turns tracking off for this page load without touching the stored opt-out. | | `feedback.theme` | `auto` (default) follows `.dark` or `[data-theme]` on ``, then the OS. Or `light` / `dark`. | | `feedback.primaryColor` | Accent colour for buttons, any CSS colour | | `feedback.strings` | Text overrides per language, such as `{ en: { title: 'Report a problem' } }` | | `feedback.zIndex` | Stacking order of the overlay | | `captureConsoleErrors` | Records `console.error` calls as breadcrumbs. Off by default. | ## In-app feedback ```ts theme={"dark"} const result = await Lerix.feedback.show(); // { submitted, id?, clientId? } ``` The dialog (message, labels, screenshot with pen annotation, email, review with **Show details**, thanks) renders inside a Shadow DOM, so your CSS can't reach it and it can't break your layout. The UI and the screenshot library are a separate entry point (`@lerix-dev/lerix-core/feedback`), loaded only when the dialog or the NPS card is first used, so bundlers put them in their own chunk. Override the dashboard settings for one call: ```ts theme={"dark"} await Lerix.feedback.show({ labels: ['bug', 'improvement'], // only these keys, in this order email: 'required', // 'hidden' | 'optional' | 'required' screenshot: 'optional', // 'hidden' | 'optional' prefillEmail: 'user@example.com', custom: { plan: 'pro' }, // up to 50 keys / 16 KB }); Lerix.feedback.setUserEmail('user@example.com'); // prefill for every dialog ``` ### Hide sensitive content Add `data-lerix-mask` to any element to black it out of screenshots: ```html theme={"dark"}

4242 4242 4242 4242

``` The dialog also offers **pixel-perfect capture**, which uses the browser's screen-share prompt to capture canvas, video and cross-origin content exactly. ### Your own form ```ts theme={"dark"} const result = await Lerix.feedback.submit({ message: 'Checkout button does nothing', labels: ['bug'], email: 'user@example.com', screenshots: [blob], // PNG, JPEG or WebP Blobs, up to 5 MB each, at most 5 custom: { plan: 'pro' }, }); // result: { clientId, id?, status: 'created' | 'duplicate' | 'queued' } ``` Feedback is queued in IndexedDB (with a `localStorage` fallback) and retried until the server accepts it. `status` is `queued` and `id` is missing while the browser is offline. ## NPS survey ```ts theme={"dark"} const shown = await Lerix.surveys.maybeShowNps(); // only when this install is due await Lerix.surveys.maybeShowNps({ force: true }); // always (testing) ``` A card slides up from the bottom: a 0 to 10 score, a follow-up question for the score's group, then thanks. On the web, an "app start" is a new session, so `minAppStarts` counts visits after 30 minutes of inactivity. See [When the survey is shown](/modules/nps-surveys#when-the-survey-is-shown). In debug mode, the console says why a survey was skipped. ## Analytics ```ts theme={"dark"} Lerix.track('checkout_started', { items: 3, coupon: 'EID' }); Lerix.screen('Checkout'); // a screen the URL doesn't name await Lerix.analytics.flush(); // send now Lerix.analytics.setEnabled(false); // opt out: persisted, clears the queue ``` * `$first_open`, `$session_start` / `$session_end` (per tab, a new session after 30 minutes of inactivity), `$app_update` (when `release` changes) and `$screen_view` (from `pushState`, `replaceState` and `popstate`) are recorded automatically. * If your router doesn't change the URL, or you want cleaner names, call `Lerix.analytics.disableAutoScreens()` and record screens yourself with `Lerix.screen()`. * `track` and `screen` never throw. Invalid events and properties are dropped on the device. * Events are batched and sent with `fetch(..., { keepalive: true })` when the tab is hidden, so the last events of a visit aren't lost. * `Lerix.analytics.getSessionId()` returns the current session ID, which error reports also carry. > [!NOTE] > > Call [`Lerix.setUser`](/modules/users-identity) after sign-in. Analytics > then counts the person across devices, and you can reply to their feedback by > push. ## Breadcrumbs ```ts theme={"dark"} Lerix.addBreadcrumb('Applied coupon', 'ui', { code: 'EID' }); ``` The last 50 breadcrumbs (navigation is added automatically) are attached to feedback. Categories: `navigation`, `log`, `http`, `ui`, `custom`. # Web — Push Notifications Source: https://docs.lerix.dev/frameworks/web/push-notifications Browser push notifications for Lerix — standard Web Push (VAPID), no Firebase, no OneSignal. ## Introduction Lerix delivers web push using the standard **Web Push protocol (VAPID)** — the same open standard Chrome, Firefox, Edge, and modern Safari all implement natively. No Firebase project, no OneSignal account — Lerix generates and manages the key pair for you. > [!NOTE] > > This is a separate package from the Flutter SDK — `@lerix-dev/lerix-js` > is a small, framework-agnostic JS/TS library for **non-Flutter** web apps > (React, Vue, plain HTML, etc.). If your app is built with Flutter — > including Flutter Web — don't use this package; the `lerix` Flutter > package supports iOS, Android, and Web natively from one package with one > API. See [Flutter — Push Notifications](/frameworks/flutter/push-notifications) > instead. ## 1. Install ```bash theme={"dark"} npm install @lerix-dev/lerix-js ``` Or drop the pre-built script directly into a page with no build step: ```html theme={"dark"} ``` ## 2. Copy the service worker Web Push requires a service worker file served from your site's own origin — a plain `