# 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 `