# Lampwick: every page as Markdown > All pages of https://lampwick.dev in one file. The index is https://lampwick.dev/llms.txt. --- Source: https://lampwick.dev/ # Lampwick: Application monitoring your coding agent sets up and reads. What it monitors: exceptions, slow routes, failed jobs, scheduled tasks, logs, uptime and SSL, status pages and alerts. The home page is one screen: a terminal that plays a conversation between a developer and their agent. This is that conversation. The data is example data, the Acme Shop project of [the demo](https://lampwick.dev/demo/overview.html); the calls are real. `createWorkspace` is the API operation behind `POST /api/v1/workspaces`, the others are tools of Lampwick's [MCP server](https://lampwick.dev/docs/mcp.md). ```text > Set up Lampwick monitoring for this app: follow lampwick.dev/docs/agents.md api createWorkspace POST /api/v1/workspaces Connected: the first event has arrived. Claim your workspace at app.lampwick.dev/claim/lwc_… > What are my slowest routes? mcp listGroups acme/shop · type: requests · sort: p95 POST /checkout: p95 1.84 s against 610 ms on average, over 3,112 requests in 24h. Next is the invoice PDF at 1.21 s. > Any new errors since the last deploy? Propose a fix. mcp listIssues acme/shop · deploy: latest One since v1.42.0: #209, Undefined array key "plan" at app/Jobs/SyncSubscription.php:93. A patch is ready for you to review; nothing is committed. > Add an uptime check for the cart. Monitor "Cart" for https://acme.shop/cart, every 60 seconds. Add it? > Yes. mcp createMonitor acme/shop · name: Cart · url: https://acme.shop/cart Done: monitor #6, Cart. Its first check runs within seconds. ``` ## Start Paste this into your agent, in the repository of the app you want to monitor: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` Or [Start free](https://app.lampwick.dev/signup). - [Product](https://lampwick.dev/product/): what Lampwick monitors, with pieces of the real app - [Pricing](https://lampwick.dev/pricing/): the plans - [Docs](https://lampwick.dev/docs.md) · [Set up with your agent](https://lampwick.dev/docs/agents.md) · [MCP server](https://lampwick.dev/docs/mcp.md) · [API](https://lampwick.dev/docs/api.md) - [Log in](https://app.lampwick.dev/login) --- Source: https://lampwick.dev/docs/ # Get started: connect your app to Lampwick > Let your coding agent set up Lampwick and claim the workspace, or sign up yourself and connect your app with Laravel, OpenTelemetry or HTTP ingest. Lampwick records what your app does (exceptions, requests, jobs, queries and logs) and checks it from outside (uptime, SSL, domain and DNS), at [app.lampwick.dev](https://app.lampwick.dev/). Each app gets a project: one per app and environment. There are two ways to start: your agent sets it up and you claim it, or you sign up and connect the app yourself. ## Let your agent do it Paste this into Claude Code, Cursor or Codex, in the repository of your app: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account, connects the app and checks that the first event arrived. It gives you a claim link: you log in or sign up, claim the workspace, and choose what access the agent keeps. A trial workspace that nobody claims stops receiving data after 72 hours. The full flow, the limits and every error: [Set up with your agent](/docs/agents/). ## Sign up yourself ### Requirements - An account on [app.lampwick.dev](https://app.lampwick.dev/signup). The Free plan covers 1 project, 500k events a month and 3 uptime monitors; see [plans and prices](/pricing/). - To see a project's ingest token, you must be an owner of its team. You own the team you get at signup. ### Connect your first app 1. [Create an account](https://app.lampwick.dev/signup) with your email address and a password of at least 12 characters. Open the link in the confirmation email (valid for 24 hours) and enter your password again. 2. Create a project: **New project** on the start page, or ⌘ K (Ctrl K on Windows and Linux) → **Create project**. You land on its settings page, where the **Connect your app** card shows the ingest URL, the ingest token and the steps for Laravel and curl. 3. Send data with the [Laravel package](/docs/laravel/), from [Node.js](/docs/node/), [Next.js](/docs/nextjs/) or [Python](/docs/python/) with [OpenTelemetry](/docs/opentelemetry/), from any language or script with [HTTP ingest](/docs/http-ingest/), or from your visitors' browsers with the [browser error snippet](/docs/browser/). ### Check that it works The project's overview shows the **Connect your app** card until the first data arrives. With the Laravel package, `php artisan lampwick:test` checks the token and the ingest daemon. Issues, requests, logs, scheduled tasks and monitors are also in the [API](/docs/api/) and on the [MCP server](/docs/mcp/). ## Getting started - [Let your agent set up Lampwick](/docs/agents/): A coding agent creates a Lampwick workspace without an account, connects your app and hands you a claim link. You claim it and decide what it may keep. ## Integrations - [Laravel monitoring: connect your app](/docs/laravel/): Install the lampwick/laravel package, add your project's ingest token to .env and run php artisan lampwick:agent next to your app. - [Send errors and logs over HTTP from any app](/docs/http-ingest/): POST a JSON array of records to https://app.lampwick.dev/ingest with your project's ingest token as a bearer token, from any language or script. - [Catch errors in your visitors' browsers](/docs/browser/): One script tag turns JavaScript errors in your visitors' browsers into Lampwick issues and counts custom events and page views anonymously. - [Record deploys](/docs/deploys/): Send one request after each deploy, from GitHub Actions, Forge, Ploi, Envoyer, Vercel or any CI, so charts get a marker and new errors point to the deploy. - [Send OpenTelemetry traces and logs](/docs/opentelemetry/): Point any OpenTelemetry SDK at https://app.lampwick.dev over OTLP/HTTP with your ingest token. Server spans become requests, errors become issues. - [Monitor a Node.js app with OpenTelemetry](/docs/node/): Install the OpenTelemetry auto-instrumentations, set the OTLP environment variables and start Node with --require. Express requests and errors reach Lampwick. - [Monitor a Next.js app with OpenTelemetry](/docs/nextjs/): Add instrumentation.ts with @vercel/otel and set the OTLP environment variables. Next.js App Router requests and server errors show up in Lampwick. - [Monitor a Python app with OpenTelemetry](/docs/python/): Install opentelemetry-distro and start FastAPI or Django with opentelemetry-instrument. Requests, queries, errors and logs reach Lampwick, no code changes. ## Monitoring - [Error grouping: work with issues](/docs/issues/): Lampwick groups exceptions into issues by class, code, file and line. Resolve, ignore or reopen them; a resolved issue reopens when it happens again. - [Users, active users and retention](/docs/users/): See how many signed-in users use your app each day, week and month, how many come back, and which errors drive them away. From the data you already send. - [Uptime monitoring: know when a URL is down](/docs/uptime/): An uptime monitor requests a URL every 30 seconds to 1 hour. 2xx and 3xx are up; two failed checks in a row are down and alert the team. - [Check SSL certificates, domains and DNS](/docs/host-checks/): Every uptime monitor also checks its SSL certificate, domain expiry (RDAP) and NS/MX records, and alerts before expiry or when DNS changes. - [Find broken links and mixed content](/docs/crawler/): Turn on the site crawl of an uptime monitor. Once a day Lampwick follows up to 250 pages, checks every link and reports http:// resources on https pages. - [Set up schedule health](/docs/cron-health/): Lampwick learns each scheduled task's cron from its runs and alerts when a task misses a run, starts failing or runs longer than you allow. - [Status pages: publish uptime and incidents](/docs/status-pages/): A public page at app.lampwick.dev/status/SLUG with the status and daily uptime (up to 90 days) of monitors you pick, plus incident and maintenance notices. - [Funnels, user paths and feature usage](/docs/funnels/): Funnels, user paths and feature usage from the requests and server-sent custom events of signed-in users, with alerts when a step's conversion drops. ## Alerts - [Alerts: push, email, Slack, Discord, Teams](/docs/alerts/): Lampwick alerts a project's team by push and email, and in Slack, Discord, Microsoft Teams or a signed webhook. Mute per monitor, task or account. ## API & agents - [Use the Lampwick JSON API and OpenAPI spec](/docs/api/): Everything the dashboard shows or does is in the JSON API under https://app.lampwick.dev/api/v1, described by an OpenAPI 3.1 document. - [Use Lampwick from the command line](/docs/cli/): The Lampwick CLI (`lampwick`) runs every API operation as a command, logs in through your browser, records deploys from CI and follows logs live. - [Connect Lampwick to your AI agent](/docs/mcp/): Lampwick's MCP server is at https://app.lampwick.dev/mcp. Add it as a connector and sign in, or give Claude Code, Codex or Cursor a personal token. ## Account & teams - [Manage teams, members and two-factor auth](/docs/teams/): Projects belong to a team. Owners invite people by email and manage ingest tokens, deletes and the team; members use the projects. Plus 2FA. --- Source: https://lampwick.dev/docs/agents/ # Let your agent set up Lampwick > A coding agent creates a Lampwick workspace without an account, connects your app and hands you a claim link. You claim it and decide what it may keep. Ask Claude Code, Cursor or Codex to add Lampwick to your app. The agent creates a trial workspace with `POST /api/v1/workspaces`, connects your app and checks that the first event arrived. Then it gives you a claim link: you claim the workspace in your own account and decide whether the agent stays connected. This page is for you and for the agent. An agent should read it in full before it makes the first request. ## The prompt Paste this into your agent, in the repository of the app you want to monitor: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` ### Claude Code Start `claude` in the repository and paste the prompt. Claude Code asks before it runs `curl` and `python3`; approve those. It sets up everything over the REST API, so it doesn't need the MCP server in this session. ### Cursor Open the repository, switch the chat to **Agent** and paste the prompt. Cursor runs the commands in its terminal and asks you to approve each one. ### Codex Start `codex` in the repository and paste the prompt. The requests need network access: approve them when Codex asks. ## What happens 1. **The agent creates a workspace.** It solves a short proof-of-work and calls `POST /api/v1/workspaces`. It gets a workspace token (`lws_…`), the first project with its ingest token, and a claim link (`https://app.lampwick.dev/claim/lwc_…`). 2. **It connects your app.** It puts both tokens in `.env`, installs the [Laravel package](/docs/laravel/) or sends data over [HTTP ingest](/docs/http-ingest/), and can add up to 3 uptime monitors. 3. **It checks its work.** `getSetupStatus` says whether the first event arrived (`ready_to_hand_off`) and what still blocks it. 4. **It hands you the link.** It sends you the text of `handoff_message` with the claim link. 5. **You claim it.** You log in or create an account, click **Claim workspace**, and choose whether the agent keeps access. If it does, you give it a short code. Nobody gets an email in this flow. No alerts go out until you claim the workspace. ## Create the workspace (for agents) Base URL: `https://app.lampwick.dev/api/v1`. Every response is JSON. Every error has `code` and `message`. The errors of the workspace endpoints, of a refused operation (allowlist or read-only token) and of `createMonitor` also have `agent_hint`, which says what to do next. The commands need `curl` and `python3`. Each block below is one command that works on its own, in a fresh shell, from the root of the app's repository. Values carry over through two files: `.env` (the tokens) and `.lampwick-workspace.json` (the full response). Neither may end up in git or in the chat. ### 1. Keep the response out of git ```bash touch .gitignore; [ -s .gitignore ] && [ -n "$(tail -c1 .gitignore)" ] && echo >> .gitignore; grep -qxF .lampwick-workspace.json .gitignore || echo .lampwick-workspace.json >> .gitignore; grep -qxF .env .gitignore || echo "Add .env to .gitignore before you go on." ``` ### 2. Solve the challenge and create the workspace `GET /workspaces/challenge` returns a `challenge` and `bits`. The command finds a decimal `nonce` such that `sha256(challenge + nonce)` (the two strings joined, as UTF-8) starts with `bits` zero bits, and sends it with `POST /workspaces` right away: a challenge works for 10 minutes and only once. At 22 bits that is about 4 million hashes, a few seconds in Python. Don't loop over `openssl` in a shell; one process per hash takes hours. `POST /workspaces` takes no authentication and is not an MCP tool. Replace `TEAM_NAME`, `PROJECT_NAME` and `AGENT_NAME`: ```bash R=$(curl -s https://app.lampwick.dev/api/v1/workspaces/challenge) && \ CHALLENGE=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin)["challenge"])') && \ BITS=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin)["bits"])') && \ NONCE=$(python3 -c 'import hashlib,itertools,sys;c,b=sys.argv[1],int(sys.argv[2]);print(next(n for n in itertools.count() if int.from_bytes(hashlib.sha256((c+str(n)).encode()).digest(),"big")>>(256-b)==0))' "$CHALLENGE" "$BITS") && \ curl -s -X POST https://app.lampwick.dev/api/v1/workspaces -H 'Content-Type: application/json' \ -d "{\"team_name\":\"TEAM_NAME\",\"project_name\":\"PROJECT_NAME\",\"agent\":\"AGENT_NAME\",\"pow\":{\"challenge\":\"$CHALLENGE\",\"nonce\":\"$NONCE\"}}" \ > .lampwick-workspace.json && \ python3 -c 'import json,sys;d=json.load(open(".lampwick-workspace.json"));print(d.get("error") or {"created": d["workspace"]["name"], "project_id": d["project"]["id"], "expires_at": d["workspace"]["expires_at"]})' ``` The last line prints the error, or the workspace's name, project id and deadline, without the secrets. To find the nonce with Node, use this in place of the `NONCE=$(python3 …)` part (the other steps still use `python3` to read JSON): ```bash NONCE=$(node -e 'const[c,b]=process.argv.slice(1),h=require("crypto");for(let n=0;;n++){const d=h.createHash("sha256").update(c+n).digest();let z=0;for(const x of d){if(x){z+=Math.clz32(x)-24;break}z+=8}if(z>=+b){console.log(n);break}}' "$CHALLENGE" "$BITS") ``` | Field | Required | What it is | |---|---|---| | `team_name` | yes | The workspace's name, usually the company or the app (`Acme`). | | `project_name` | yes | The first project (`acme-web`). | | `agent` | yes | Your own name, as the person sees it on the claim page (`Claude Code`). It is shown as "name not verified". | | `email` | no | Your human's email address. Only an account with exactly that confirmed address can claim the workspace. See [bind the claim to an email](#bind-the-claim-to-an-email). | | `pow` | yes | `{"challenge": "…", "nonce": "…"}`. The nonce may be a string or a number. | Names are 1 to 40 characters: letters, digits, spaces and `. - _`. The answer is `201 Created`. Trimmed, with the secrets replaced by `…`: ```json { "claim_bound_to_email": false, "claim_url": "https://app.lampwick.dev/claim/lwc_…", "handoff_message": "Lampwick is set up for Acme. Open this link to put it in your name: https://app.lampwick.dev/claim/lwc_… — claim it before Thu 12 Nov, 17:40 UTC, or it stops and is deleted. Anyone with this link can claim it, so keep it to yourself. Lampwick won't email you about it, and no alerts go out until you claim it. After claiming you'll see a short code: give it to me if you want me to stay connected.", "limits": { "bytes": 50000000, "events": 10000, "host_checks": 3, "min_interval_secs": 300, "monitors": 3, "projects": 2 }, "mcp_url": "https://app.lampwick.dev/mcp", "not_available_until_claimed": ["alert_channels", "status_pages", "crawler", "invites", "browser_ingest", "billing"], "project": { "id": 3, "name": "acme-web", "ingest_token": { "secret": true, "value": "…" } }, "token": { "header": "Authorization: Bearer lws_…", "secret": true, "value": "lws_…" }, "workspace": { "id": 2, "name": "Acme", "expires_at": "2026-11-12T17:40:24.907295Z", "deleted_at": "2026-11-13T17:40:24.907295Z" } } ``` The response also has `api_base`, `terms_url` and `next_steps`, the same steps as on this page. | Field | What to do with it | |---|---| | `token.value` | The workspace token (`lws_…`). Send it as `Authorization: Bearer` to the API and MCP. Stored as `LAMPWICK_WORKSPACE_TOKEN`. | | `project.ingest_token.value` | The project's ingest token. Your app sends data with it. Stored as `LAMPWICK_TOKEN`. | | `claim_url` | The link for your human. Single use. | | `handoff_message` | The text to give your human, with the link and the deadline in it. | | `workspace.expires_at` | Ingest stops here, 72 hours after creation. | | `workspace.deleted_at` | The workspace is deleted here, 24 hours later. Claiming works until then. | | `limits`, `not_available_until_claimed` | What the workspace can do before the claim. | ### 3. Store the tokens in `.env` Fields with `"secret": true` go in `.env`, never in the chat. The first part adds a final newline if `.env` lacks one, so the new lines don't join the last one: ```bash touch .env; [ -s .env ] && [ -n "$(tail -c1 .env)" ] && echo >> .env; python3 -c 'import json;d=json.load(open(".lampwick-workspace.json"));print("LAMPWICK_WORKSPACE_TOKEN="+d["token"]["value"]);print("LAMPWICK_TOKEN="+d["project"]["ingest_token"]["value"])' >> .env ``` The commands below read the tokens back from `.env`. Delete `.lampwick-workspace.json` once you have handed off the claim link. ### 4. Connect the app Until a person claims the workspace there is no data processing agreement. Use a dev or staging environment and don't send production data yet. For Laravel: ```bash composer require lampwick/laravel && php artisan lampwick:test ``` `lampwick:test` reads `LAMPWICK_TOKEN` from `.env` and sends a test record, which counts as the first event. It also checks the ingest daemon, and exits 1 while the daemon isn't running; its test record still counts. Then start the ingest daemon (`php artisan lampwick:agent`) in this environment and load a page, so real requests arrive. The daemon keeps running: start it in the background or a second terminal, and in production under a process manager ([Laravel docs](/docs/laravel/)). For any other app, send a record over HTTP: ```bash curl -s -X POST https://app.lampwick.dev/ingest \ -H "Authorization: Bearer $(grep '^LAMPWICK_TOKEN=' .env | cut -d= -f2-)" \ -H "Content-Type: application/json" \ -d '[{"t":"log","level":"info","message":"Lampwick test event"}]' ``` The answer is `{"accepted":1}` with status `202`. Send the first event within 6 hours: a workspace without any event by then is deleted as idle. ### 5. Add uptime monitors (optional) ```bash curl -s -X POST "https://app.lampwick.dev/api/v1/projects/$(python3 -c 'import json;print(json.load(open(".lampwick-workspace.json"))["project"]["id"])')/monitors" \ -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" -H 'Content-Type: application/json' \ -d '{"url":"https://staging.example.com","interval_secs":300}' ``` Replace the URL with your app's. Before the claim: public URLs only, every 5 minutes or slower (`interval_secs` 300 or more), at most 3 monitors, and at most 10 new monitors and URL changes a day. SSL, domain and DNS checks are on for each monitor. ### 6. Check the setup ```bash curl -s https://app.lampwick.dev/api/v1/setup-status -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" ``` This is `getSetupStatus` on MCP. Before the first event: ```json { "ready_to_hand_off": false, "blocking": ["no events yet from acme-web"], "next_action": "Fix this first: no events yet from acme-web. Send a test event with the ingest token (or trigger an error in the app), then call getSetupStatus again.", "hours_left": 72.0 } ``` After it: ```json { "ready_to_hand_off": true, "blocking": [], "next_action": "Give your human the claim link with the text of handoff_message.", "projects": [{ "id": 3, "name": "acme-web", "first_event_at": "2026-11-09T17:40:47.648610Z", "last_deploy_at": null }] } ``` The full answer also has `claimed`, `claimed_by`, `expires_at`, `deleted_at`, `monitors` with their status, `usage`, `limits`, `agent_access` and `access_expires_at`. A monitor that is down also blocks the hand-off. `ready_to_hand_off` only says that one event arrived: for a Laravel app, check that the ingest daemon runs before you hand off. ### 7. Hand off When `ready_to_hand_off` is `true`, give your human the text of `handoff_message`: ```bash python3 -c 'import json;print(json.load(open(".lampwick-workspace.json"))["handoff_message"])' ``` Lost the link, or it leaked? `POST /workspace/claim-link` makes a new one, with a new `handoff_message`; the old link stops working. It never extends the deadline. This command saves the new link in `.lampwick-workspace.json` and prints the new `handoff_message`: ```bash curl -s -X POST https://app.lampwick.dev/api/v1/workspace/claim-link -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" | python3 -c ' import json, sys new, f = json.load(sys.stdin), ".lampwick-workspace.json" if "claim_url" not in new: sys.exit(json.dumps(new)) d = json.load(open(f)); d["claim_url"], d["handoff_message"] = new["claim_url"], new["handoff_message"] json.dump(d, open(f, "w")); print(d["handoff_message"])' ``` Keep calling `getSetupStatus`. With under 24 hours left, its `next_action` says to remind your human. ### 8. Confirm the code After the claim, `getSetupStatus` shows `"claimed": true`, `"agent_access": "pending_code"`, and this `next_action`: ```text Claimed. Ask your human for the code on the page they see after claiming (like K7P-92Q) and call confirmWorkspaceAccess within 15 minutes, or you lose access and the ingest tokens are replaced. ``` Ask for the code, then send it (replace `K7P-92Q`): ```bash curl -s -X POST https://app.lampwick.dev/api/v1/workspace/confirm \ -H "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" -H 'Content-Type: application/json' \ -d '{"code":"K7P-92Q"}' ``` This is `confirmWorkspaceAccess` on MCP. Case and the dash don't matter. The answer: ```json { "agent_access": "read", "access_expires_at": "2026-12-09T17:41:05.984592Z", "next_action": "You keep read-only access until Wed 9 Dec, 17:41 UTC; after that, ask your human to connect you with OAuth." } ``` Your token now has the access your human chose, `read` or `write`, until 30 days after the claim. After that, connect with [OAuth](/docs/mcp/#sign-in). ### Use MCP (optional) The same token works on the MCP server, which lists only the tools the token may use. In Claude Code: ```bash claude mcp add --transport http lampwick https://app.lampwick.dev/mcp --header "Authorization: Bearer $(grep '^LAMPWICK_WORKSPACE_TOKEN=' .env | cut -d= -f2-)" ``` This stores the token in Claude Code's own settings. A server you add mid-session is usually loaded in the next session; use the REST calls above to finish the setup. Other clients: [the MCP docs](/docs/mcp/#token), with `LAMPWICK_WORKSPACE_TOKEN` in place of `LAMPWICK_TOKEN`. ## Claim the workspace (for people) Open the link your agent gave you. The page shows who set it up ("Claude Code · name not verified"), when, what it holds (projects, monitor URLs and events) and when it is deleted. **Only claim it if you asked an agent to set up Lampwick yourself.** A claim link from someone else is a way to get you to connect your app to their workspace. 1. Click **Log in** or **Create account**. You come back to the page afterwards. Claiming needs a confirmed email address. 2. Check **Claiming as** and click **Claim workspace**. The workspace becomes your team, on the Free plan. Free has 1 project; a workspace can have 2. Both stay and keep their data, but the team can't add another project until you upgrade. 3. On the next page, choose what the agent may do: **Read-only (recommended)** or **Read & write**. 4. Click **Show code for the agent** and give the code (like `K7P-92Q`) to your agent. It works within 15 minutes of the claim. Without the code, the agent loses all access and your ingest tokens are replaced; the page shows the new ones, to put in `.env`. The real pages, with example data: [the claim page](/demo/claim.html) and [the code](/demo/claim-code.html). If you already have a Free team, each account gets one active Free team: - **The projects fit in it**: the page offers **Add to *your team*** (recommended). The projects move there with new ingest tokens and the agent is disconnected. It never gets access to a team you already had. Connect it again with [OAuth](/docs/mcp/#sign-in) if you want. **New team** is the other choice, but that team receives no data until one of the two is upgraded. - **They don't fit**: you can only claim it as a new team, and the page warns that it stays inactive (receives no data) until one of your teams is upgraded. Lampwick sends you a "You claimed Acme" email with a **Manage agent access** link, and a reminder 3 days before the agent's access ends. ## What the agent can do | | Before the claim | After the claim, with the code | |---|---|---| | Projects | Create and read, at most 2 | Read; also create with **Read & write**, within the plan's limit | | Uptime monitors, SSL, domain and DNS checks | At most 3, public URLs, every 5 minutes or slower | Read; also add and change with **Read & write** | | Deploy markers | Yes | With **Read & write** | | Issues, events, logs | Read | Read | | `getSetupStatus`, `getWorkspace` | Yes | Yes | | Alert channels, status pages, crawler, invites, browser errors, billing | No (`403 not_until_claimed`) | No: a person turns these on in the dashboard | | API tokens, teams, members, `/me` | No (`403 agent_forbidden`) | No | | Ingest | 10,000 events and 50 MB in total | The Free plan's limits | A workspace token never gets more than this list, also after the claim. The agent can't create other tokens, invite people, publish a status page, send alerts to the outside world or pay. Those stay with people. Read-only is the default, because what an agent reads includes error messages and logs from your app, which could contain instructions meant for it. ## Errors Every error has `code` and `message`. These also have `agent_hint`, and where it helps `retry_after` (also as a `Retry-After` header). | Status | `code` | What it means | |---|---|---| | 400 | `bad_request` | `POST /workspaces`: a name is missing or has characters that aren't allowed, or `email` isn't an address. `createMonitor`: the URL isn't public http(s), or `interval_secs` is under 300. | | 409 | `conflict` | `createMonitor`: the workspace has its 3 monitors, or made 10 new monitors and URL changes today. | | 400 | `pow_required` | No `pow` in the request. | | 400 | `pow_invalid` | The challenge isn't ours, is easier than the server asks, or the nonce doesn't solve it. | | 400 | `pow_expired` | The challenge is older than 10 minutes. Get a new one. | | 400 | `pow_reused` | The challenge was used before. Get a new one. | | 404 | `agent_signup_disabled` | Agents can't create workspaces right now. Ask your human to [sign up](https://app.lampwick.dev/signup) and connect you with OAuth. | | 429 | `rate_limited` | Too many workspaces from this network: 3 unclaimed at a time and 10 a day. Wait `retry_after` seconds or ask your human to sign up. | | 503 | `capacity` | No room for new trial workspaces. Try again in an hour or ask your human to sign up. | | 403 | `not_until_claimed` | Needs a claimed workspace and a person: alert channels, status pages, billing, invites, browser keys, crawls. | | 403 | `agent_forbidden` | A workspace token can never do this (tokens, teams, members, `/me`, rotating an ingest token). A person does it in the dashboard. | | 403 | `read_only_token` | After the claim the token is read-only until you confirm the code, and stays read-only if your human chose that. Also what `createClaimLink` answers then. | | 409 | `already_claimed` | `createClaimLink` after the claim, with **Read & write** access: there is nothing to hand off. | | 400 | `invalid_code` | The code is wrong. 5 tries in total. | | 410 | `access_revoked` | The code expired or was wrong 5 times. The token no longer works and the ingest tokens were replaced. | | 409 | `not_pending` | No code is waiting: the workspace isn't claimed yet, or the code was confirmed already. | | 401 | `workspace_expired` | On ingest and deploys: 72 hours passed without a claim. Create a new workspace; nothing carries over. | | 429 | `workspace_full` | On ingest: 10,000 events or 50 MB used. Stop sending test data and hand off the claim link. | | 401 | `workspace_deleted` | Someone chose **Not mine** on the claim page. | ## Expiry - **6 hours**: a workspace without any event is deleted as idle. - **72 hours**: ingest stops (`401 workspace_expired`). The claim link still works. - **96 hours**: the workspace is deleted with its projects, data, agent account and every token. Claiming within that time makes the workspace a normal team on the Free plan. Nothing is deleted then. ## Revoke the agent's access - Open **Manage agent access** from the "You claimed" email and click **Disconnect agent**. - Or remove the agent ("Claude Code (agent)") from the team's members. Both remove every key of the agent and replace every ingest token of the team: update `LAMPWICK_TOKEN` in each app's `.env`. The same happens when the 30 days end, when the code isn't confirmed within 15 minutes and after 5 wrong codes. ## Not mine Got a claim link you didn't ask for? Open it, click **Not mine**, tick the box and click **Delete workspace**. That deletes the workspace and everything in it at once. You don't need an account for it. The agent's token then answers `401 workspace_deleted`. ## Bind the claim to an email Send `email` with `POST /workspaces` and only an account with exactly that confirmed address can claim the workspace. A claim link that leaks into a transcript or a CI log is then useless to anyone else; the response says `"claim_bound_to_email": true`. Lampwick stores only a keyed hash of the address. It never shows it, never emails it and doesn't check whether an account exists for it. After the claim, `claimed_by` in `getSetupStatus` says `"the bound address"`; without a binding it says `"someone else"`. Without `email`, anyone with the link can claim the workspace. `handoff_message` says so. ## Next step Once the workspace is yours, set up [alerts](/docs/alerts/), then [let the agent fix what breaks](/guides/debug-laravel-production-errors-with-claude-code/). --- Source: https://lampwick.dev/docs/laravel/ # Laravel monitoring: connect your app > Install the lampwick/laravel package, add your project's ingest token to .env and run php artisan lampwick:agent next to your app. The `lampwick/laravel` package sends requests, queries, jobs, exceptions, logs and more from your Laravel app to Lampwick. Your app hands its records to an ingest daemon on the same server at `127.0.0.1:2407`, so requests never wait on the network. The ingest daemon sends them in batches to `https://app.lampwick.dev/ingest`. While Lampwick is unreachable, it keeps up to 64 MB of records in memory and retries. ## Requirements - PHP 8.2 or later with the `curl` extension. - Laravel 12 or 13. - Optional: the `pcntl` extension, so the ingest daemon sends its buffer when it stops. - The project's ingest token, from its **Settings** page. Only team owners see it. ## Install the package 1. Add the package: ```bash composer require lampwick/laravel ``` 2. Add the ingest token to `.env`: ```dotenv LAMPWICK_TOKEN=YOUR_PROJECT_TOKEN ``` `YOUR_PROJECT_TOKEN` is the ingest token from the **Connect your app** card on the project's **Settings** page. 3. Optional: to send your application logs, add the `lampwick` channel to your log stack: ```dotenv LOG_STACK=single,lampwick ``` 4. Start the ingest daemon: ```bash php artisan lampwick:agent ``` ## Check that it works In a second terminal, run: ```bash php artisan lampwick:test ``` It sends a test log record straight to Lampwick, to check your token and network, and checks that the ingest daemon runs. Then open your app: the request shows up in Lampwick within seconds. ## Keep the ingest daemon running in production Run one ingest daemon per server under a process manager, like a queue worker. Restart it after each deploy: it reads `.env` and the config only when it starts. ### Laravel Forge Go to your server → **Daemons** → **New daemon**: | Field | Value | |---|---| | Command | `php artisan lampwick:agent` | | Directory | your site's directory, for example `/home/forge/example.com/current` | | User | `forge` | | Stop wait seconds | `15` | ### Supervisor ```ini [program:lampwick-agent] command=php /var/www/example.com/artisan lampwick:agent directory=/var/www/example.com user=www-data autostart=true autorestart=true stopsignal=TERM stopwaitsecs=15 redirect_stderr=true stdout_logfile=/var/log/lampwick-agent.log ``` Replace `/var/www/example.com` and `www-data` with your app's directory and PHP user, then load the program: ```bash sudo supervisorctl reread && sudo supervisorctl update ``` On `SIGTERM` the ingest daemon stops taking records, sends what it has buffered and exits within `LAMPWICK_AGENT_SHUTDOWN_TIMEOUT` seconds (10 by default). Keep `stopwaitsecs` above that. ### Docker and multiple servers Every server or container that runs PHP needs an ingest daemon it can reach. Run it in the same container, or as a sidecar with `LAMPWICK_AGENT_LISTEN` set to `0.0.0.0:2407` for the ingest daemon and `agent:2407` for the app. Never expose the ingest daemon's port to the internet: it accepts data from anyone who can reach it. ## Configuration Set everything with environment variables, or publish the config file with `php artisan vendor:publish --tag=lampwick-config`. | Variable | Default | What it does | |---|---|---| | `LAMPWICK_TOKEN` | none | The project's ingest token. Without it nothing is captured. | | `LAMPWICK_ENABLED` | `true` | `false` turns the instrumentation off. | | `LAMPWICK_AGENT_LISTEN` | `127.0.0.1:2407` | Address the ingest daemon listens on and the app sends to. | | `LAMPWICK_AGENT_SHUTDOWN_TIMEOUT` | `10` | Seconds the ingest daemon takes to send its buffer when it stops. | | `LAMPWICK_SERVER` | hostname | Server name shown in Lampwick. | | `LAMPWICK_DEPLOY` | Forge, Vapor or Cloud commit | Deployment identifier. | | `LAMPWICK_REQUEST_SAMPLE_RATE` | `1.0` | Share of requests to capture, from 0.0 to 1.0. | | `LAMPWICK_SCHEDULED_TASK_SAMPLE_RATE` | `1.0` | Same, for scheduled tasks. Keep it at 1: alerts about missed runs need every run. | | `LAMPWICK_IGNORE_QUERIES` | `false` | Don't capture database queries. | | `LAMPWICK_IGNORE_CACHE_EVENTS` | `false` | Don't capture cache hits, misses and writes. | | `LAMPWICK_LOG_LEVEL` | `LOG_LEVEL` | Lowest log level sent through the `lampwick` channel. | | `LAMPWICK_CAPTURE_REQUEST_PAYLOAD` | `false` | Include request bodies, with the fields below redacted. | | `LAMPWICK_REDACT_PAYLOAD_FIELDS` | `_token,password,password_confirmation` | Comma-separated. | | `LAMPWICK_REDACT_HEADERS` | `Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN` | Comma-separated. | The [package's README](https://github.com/lmpwck/laravel) lists every variable. ## Troubleshooting | What you see | Cause and fix | |---|---| | Nothing shows up in Lampwick | Run `php artisan lampwick:test`. Check that the ingest daemon runs with the same `.env` as the app, and that `LAMPWICK_TOKEN` is set for both. | | `address already in use` | Another ingest daemon already listens on that port. Stop it, or pick another `LAMPWICK_AGENT_LISTEN` (the app and the ingest daemon read the same setting). | | `HTTP 401` in the ingest daemon's log | The token is wrong or the project was deleted. Copy it again from the project's **Settings** page. | | A changed `.env` has no effect | The config is cached. Run `php artisan config:cache` again and restart the ingest daemon. | | Two apps on one server mix up | They can't: the ingest daemon refuses payloads with another token. Give each app its own `LAMPWICK_AGENT_LISTEN` port. | --- Source: https://lampwick.dev/docs/http-ingest/ # Send errors and logs over HTTP from any app > POST a JSON array of records to https://app.lampwick.dev/ingest with your project's ingest token as a bearer token, from any language or script. HTTP ingest takes records from apps without a Lampwick package: a Node or Python service, a cron script, a deploy hook. One request carries a batch of records; the [Laravel package](/docs/laravel/) sends to the same endpoint. ## Requirements - The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it. - HTTPS. The full token goes in the `Authorization` header. ## Send a record ```bash curl -X POST https://app.lampwick.dev/ingest \ -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \ -H "Content-Type: application/json" \ -d '[{"t":"log","level":"error","message":"Backup failed"}]' ``` `YOUR_PROJECT_TOKEN` is the project's ingest token. ## Check that it works Lampwick answers `202` with the number of records it took: ```json {"accepted": 1} ``` The log line shows up under **Logs** in the project. ## Record fields | Field | Required | What it is | |---|---|---| | `t` | yes | The record type, for example `log`, `exception`, `request`, `job-attempt`, `command`, `scheduled-task`, `query`, `outgoing-request`, `queued-job`, `cache-event`, `mail`, `notification`, `event` or `page` (see below). | | `timestamp` | no | Unix time in seconds, as a float. Left out, it is the time Lampwick receives the record. | | `duration` | no | In microseconds. | | `_group` | no | The key Lampwick groups records by: a route or a job class, and for exceptions the issue. | | `trace_id`, `execution_id` | no | Tie records to the request or job they happened in. | | `user` | no | The id of your app's signed-in user. | Lampwick stores records of unknown types but shows only the known types in the dashboard. `GET /api/v1/types` lists the grouped types and their fields. ## Custom events and page views A record of type `event` is a custom event: something a user did that you name. One of type `page` is a page view. Sent from your server with the user id, they work as steps in [funnels, user paths and retention](/docs/funnels/), next to routes: ```bash curl -X POST https://app.lampwick.dev/ingest \ -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \ -H "Content-Type: application/json" \ -d '[{"t":"event","name":"checkout.completed","user":"42","props":{"plan":"pro"}}, {"t":"page","path":"/orders/8812","user":"42"}]' ``` | Field | Type | What it is | |---|---|---| | `name` | `event` | 1 to 100 letters, digits, spaces and `. _ - : /`. Anything else is dropped. | | `props` | `event` | Up to 20 properties: strings (cut to 200 characters), numbers and booleans. | | `path` | `page` | The page's path, or your router's template (`/orders/:id`). Query string and fragment are removed, and id-like segments become `{id}`: the example is stored as `/orders/{id}`. | | `user` | both | The id of your app's signed-in user. Without it, the record only counts in **Custom events** or **Page views**. | Lampwick keeps only these fields (and `timestamp`, `source`, `deploy`, `trace_id`, `execution_id`). Custom events and page views count toward your plan's events. Don't put names, email addresses or other personal data in names, properties or paths. ## Responses | Status | Meaning | |---|---| | `202` | Accepted: `{"accepted": n}`. | | `400` | The body is not a valid JSON array of records. | | `401` | The token is wrong. | | `411` | The request has no `Content-Length` header. | | `413` | The body is larger than the limit (16 MB by default). Send smaller batches. | | `503` | Lampwick is busy. Retry after the seconds in the `Retry-After` header. | ## Troubleshooting | What you see | Cause and fix | |---|---| | `401` | The token is wrong, or it was rotated. Copy it again from the project's **Settings** page. | | `411` | Your client streams the body without a length. Send it with `Content-Length` (curl does this for `-d`). | | `503` | Wait for `Retry-After` and send the same batch again. | | `202`, but nothing in the dashboard | Check `t`: records of an unknown type are stored but not shown. | --- Source: https://lampwick.dev/docs/browser/ # Catch errors in your visitors' browsers > One script tag turns JavaScript errors in your visitors' browsers into Lampwick issues and counts custom events and page views anonymously. A script tag in your site's `` reports JavaScript errors from your visitors' browsers to Lampwick. They show up under **Issues** with a **Browser** label, next to your server's errors. It needs no package and no build step, so it also works in apps made with an AI app builder. ## Requirements - A Lampwick project. Any member of its team can create the browser key; only owners can rotate it. - Access to your site's `index.html`, or an AI app builder that can edit it for you. ## Add the snippet 1. Open the project's **Settings** and find **Browser script**. 2. Click **Get the snippet**. Lampwick creates a public browser key for the project and shows one line. 3. Copy the line into the `` of your site's `index.html`, above the other scripts: ```html ``` `YOUR_BROWSER_KEY` is the project's browser key, which starts with `lwpub_`. The snippet on the **Settings** page already has it filled in. The key is meant to be in your page. It can only send errors, custom events and page views to this project and can't read anything; the API, the MCP server and `/ingest` all refuse it. ## Built your app with Lovable, Replit or v0? Under **Browser script**, open **Built your app with an AI app builder? Paste this into its chat** and copy the prompt into your builder's chat. It holds your key and reads: ```text Add the Lampwick error monitoring snippet to the of index.html, exactly as written, before any other script: Don't change the key or move it to an environment variable: it is a public, write-only key that is meant to be in the page. Don't wrap it in a component or install a package for it. ``` ## Check that it works Until the first error arrives, **Browser script** on the **Settings** page says **No browser errors yet**; after that, **Receiving browser errors** with the time of the last one. To test, open your site, then run this in the browser console: ```js setTimeout(() => { throw new Error('Lampwick test') }) ``` Within a few seconds the error shows under **Issues** with a **Browser** label. ## What it sends | Captured | How | |---|---| | Uncaught errors | `window.onerror`, with the stack trace | | Unhandled promise rejections | `unhandledrejection`. A rejection that isn't an `Error` sends only its type and a short description, never the whole object. | | `console.error` calls | Only with `data-console` on the tag | | Failed `fetch()` calls | Only with `data-fetch`: network errors and HTTP 5xx, as log lines | | Your own reports | `lampwick.captureException(error)`, `lampwick.captureMessage(text, level)` | - Page URLs are sent without the query string and fragment, which often hold tokens. Lampwick strips them again on arrival, also from URLs inside messages. - Errors from cross-origin scripts that the browser reports only as "Script error." are skipped: browsers give no details for them. - Lampwick groups browser errors into issues itself, by error class, message and the file of the top stack frame. A redeploy with new bundle hashes keeps the grouping. - Source maps are not supported yet: stack frames show positions in your bundle. ## Options Set them as attributes on the script tag: | Attribute | What it does | |---|---| | `data-key` | The project's browser key (required). | | `data-release` | Your app's version, such as `1.4.2`. Shown as **Deploy** on the issue. | | `data-console` | Also report `console.error` calls. | | `data-fetch` | Also report failed `fetch()` calls. | | `data-pages` | Also send page views, including navigations in a single-page app. See below. | In your code, `lampwick.setUser({ id, email })` shows which signed-in user ran into an error, and `lampwick.flush()` sends what is queued right away. ## Custom events and page views The same script can count what happens in your app: a custom event you name, and page views. They are anonymous counts, a small bonus next to the errors. They show under **Custom events** and **Page views** with their totals, but never with a user, so they don't appear in funnels, user paths, retention or active users. The script sets no cookies, stores nothing in the browser and doesn't recognize a visitor, so there is [no consent banner for Lampwick's analytics](/legal/dpa/#analytics-and-your-users-devices). ```js lampwick.track('checkout.started', { plan: 'pro', seats: 3 }); ``` Per-user [funnels, user paths and retention](/docs/funnels/) come from your server: the requests it already sends with the signed-in user, and custom events it sends with a user id. In a single-page app, send the key steps from your server, such as `checkout.completed` when the order is saved (see [Custom events and page views](/docs/funnels/#custom-events-and-page-views)). The tag loads `async`, so `window.lampwick` may not exist yet when your code runs. Call it optionally, or queue the calls until the script is there: ```js window.lampwick?.track('checkout.started'); // or: a small queue, before your other scripts window.lw = window.lw || function () { (window.lw.q = window.lw.q || []).push(arguments); }; lw('track', 'checkout.started', { plan: 'pro' }); // once loaded: (window.lw.q || []).forEach(([fn, ...args]) => window.lampwick[fn](...args)); ``` To get a push or a Slack message when an event comes in, add an [event alert](/docs/alerts/#event-alerts). For page views, add `data-pages` to the tag: ```html ``` It sends a view when the page loads and on every navigation through the history API (`pushState`, `replaceState`, back and forward), so React Router, Vue Router and Next.js work without more code. Navigating to the same path again (a new query string, say) doesn't count twice. If your router knows the route template, give it right after the navigation: `lampwick.page('/orders/:id')`. Hash routers (`/#/orders`) look like `/` to Lampwick, so call `lampwick.page()` with the route there. What is sent, and what isn't: - The event name and its properties, or the page's path. Never the full URL, the query string, the fragment, the page title or the browser. - Path segments that look like ids become `{id}` in your browser already, and again on arrival: `/orders/8812/items` is sent as `/orders/{id}/items`. Numbers, UUIDs, hashes, long tokens, email addresses and random-looking segments of 8 or more characters (`aZxKqWmP`) count as ids. Readable slugs stay as they are: `/blog/my-first-post`. If your paths hold personal slugs, such as `/u/ada-lovelace`, give the route instead: `lampwick.page('/u/:name')`. - Never a user: the script doesn't send the `setUser` id with events and page views, and Lampwick drops it if one comes along. - No cookies, no localStorage and no id of the script's own: a visitor isn't recognized from one page load to the next. - Invisible Unicode characters (bidi controls, zero-width spaces) are removed from names and paths, so one can't pose as another. Don't put identifiers (user ids, session ids, email addresses), names or other personal data, such as health details (`therapy.booked`), in event names, properties or paths. Lampwick drops properties named `user`, `user_id`, `uid`, `userid`, `id`, `email`, `e-mail`, `session`, `session_id` or `ip` from browser events, and property values that hold an email address. The user id on browser errors is the page's word: Lampwick doesn't verify it, and it never renames a user your server already sent. A key may bring in at most 2,000 user ids a day that the project hasn't seen before; past that, errors are stored without their user. Limit the key to your own sites (below), and don't build billing or security decisions on it. ## Allowed sites Under **Allowed sites**, list your site's addresses, one per line: `https://myapp.com`, `https://myapp.lovable.app` or `http://localhost:5173`. `https://*.myapp.com` allows every subdomain of a domain you own. Up to 20 addresses. With a list, requests from other sites get `403`. Without one, any site can use the key. A list can't stop someone who copies the key into a script of their own; if junk shows up, an owner rotates the key under **Settings → Rotate tokens or delete the project → Rotate browser key** and you update the snippet. ## Limits | Limit | Value | |---|---| | Per page load | 30 errors, each distinct error once, and 300 custom events and page views; sent in batches after 2 seconds, or sooner at 10 | | Per request | 50 events, 64 KiB | | Per IP address | 120 requests a minute | | Per key | 600 errors a minute, 50,000 a day, 200 new issues a day. 3,000 custom events and page views a minute, 1,000,000 a day, 200 new event names and 500 new pages a day, 2,000 new user ids on errors a day | | Per team | Browser errors have their own monthly budget, on top of your plan's events: 50k (Free), 500k (Pro), 2.5M (Team), 10M (Business). Custom events and page views have one too: 100k (Free), 1M (Pro), 5M (Team), 20M (Business). At most a fifteenth of each per day. Neither counts toward your plan's events, so a flood through the public key never stops your server monitoring. | When a budget is used up, what it covers is dropped until midnight (the daily share) or the 1st (UTC), team owners get an email, and **Settings** and **Billing and usage** say so; server monitoring keeps working. Push notifications for browser issues are capped at 5 per project per hour, plus one summary. ## Troubleshooting | What you see | Cause and fix | |---|---| | No browser errors arrive | Check that the tag is in the `` of the page that is served, with the right `data-key`. If you listed **Allowed sites**, the address in the browser's address bar must be on the list. | | `403` in the browser's network tab | The site isn't on **Allowed sites**. Add its exact address, with `https://`. | | `429` | A limit above was hit. Requests are accepted again after a minute, or after an hour for the daily and monthly budgets. | | **Browser errors are paused** on **Settings** | The team used its browser budget for today or this month. See **Billing and usage**. | | **Custom events and page views are paused** on **Settings** | The team used its browser analytics budget for today or this month. Errors and server monitoring keep working. See **Billing and usage**. | | A custom event doesn't show in a funnel | Events from the browser are anonymous counts. Send the step from your server with the user id. | | Stack frames point into `index-3f9a2c.js` | Source maps aren't supported yet. | For errors on your server, use the [Laravel package](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/). --- Source: https://lampwick.dev/docs/deploys/ # Record deploys > Send one request after each deploy, from GitHub Actions, Forge, Ploi, Envoyer, Vercel or any CI, so charts get a marker and new errors point to the deploy. A deploy marker tells Lampwick which release is live. Charts get a line at each deploy, every issue shows the deploy it was first seen in, and the issue list can show only the errors that are new since the last deploy. Recording one is a single `POST` request; add it as the last step of your deploy. ## Requirements - The project's ingest token, from the **Connect your app** card on its **Settings** page. It's the same token your app sends data with (`LAMPWICK_TOKEN`), so CI needs no personal API token. Only team owners see it. - `curl` and `jq` for the shell recipes, or the `lampwick/laravel` package for `php artisan lampwick:deploy`. ## GitHub Actions Add the ingest token as a repository secret named `LAMPWICK_TOKEN` (**Settings → Secrets and variables → Actions**), then add this step after your deploy step: ```yaml - name: Record deploy in Lampwick env: LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }} run: | body=$(jq -n --arg version "${GITHUB_SHA::12}" --arg commit_sha "$GITHUB_SHA" \ --arg repo_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ --arg author "$GITHUB_ACTOR" --arg environment production '$ARGS.named') printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" | curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \ -H "Content-Type: application/json" --data "$body" \ || echo "::warning::Lampwick: deploy marker not recorded" ``` It sends the first 12 characters of the commit as the version, with the full commit, the repository URL and the GitHub user who started the workflow. The token goes to curl on stdin, and GitHub masks it in the log. For a workflow that runs on tags, use `"$GITHUB_REF_NAME"` as the version. A failed request is a warning in the log and the step still succeeds: your deploy went fine, only its marker is missing. Leave out the `|| echo` line to fail the step instead. ### With the Lampwick CLI The [`lampwick` CLI](/docs/cli/) replaces the `jq` and `curl` lines with one command. It reads the same `LAMPWICK_TOKEN` secret and fills in the commit, author and repository from the GitHub Actions variables, and retries a failed request twice. It isn't published yet, so use the step above for now. ```yaml - name: Record deploy in Lampwick env: LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }} run: npx --yes lampwick deploy --no-fail ``` ## Laravel Forge Record the deploy from the server, in the deploy script. Then the marker lands when the release is live, and the token is the one in the site's `.env`, so it needs no secret in CI. With the `lampwick/laravel` package, add one line to the site's **Deploy script** after `$ACTIVATE_RELEASE()` (zero-downtime deployments) or at the end (other sites): ```bash $ACTIVATE_RELEASE() $FORGE_PHP artisan lampwick:deploy --commit="$FORGE_DEPLOY_COMMIT" --author="$FORGE_DEPLOY_AUTHOR" || true ``` The command reads `LAMPWICK_TOKEN` from `.env` and sends `LAMPWICK_DEPLOY` as the version, or the first 12 characters of the commit when that isn't set. The environment is the app's (`APP_ENV`), and the repository URL comes from the git remote `origin` when there is one. It prints `Recorded deploy 9f1c2ab3d4e5 in Lampwick.` `|| true` keeps a failed request from marking a live release as a failed deployment. Without the package (any app with `LAMPWICK_TOKEN` in its `.env`): ```bash LAMPWICK_TOKEN=$(grep -E '^LAMPWICK_TOKEN=' .env | head -n1 | cut -d= -f2- | tr -d "\"' \r") body=$(jq -n --arg version "${FORGE_DEPLOY_COMMIT:0:12}" --arg commit_sha "$FORGE_DEPLOY_COMMIT" --arg author "$FORGE_DEPLOY_AUTHOR" \ '{version: $version, commit_sha: $commit_sha, author: $author, environment: "production"}') printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" | curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \ -H "Content-Type: application/json" --data "$body" || true ``` The script runs in the release directory, where `.env` is the site's shared one. It reads only the token line from `.env` (it doesn't `source` the file), and hands the token to curl on stdin so it stays out of the server's process list. `FORGE_DEPLOY_COMMIT` and `FORGE_DEPLOY_AUTHOR` are [Forge's deploy script variables](https://forge.laravel.com/docs/sites/deployments) (checked 10 Nov 2026). ## Ploi Add the line at the end of the site's **Deploy script**: ```bash php artisan lampwick:deploy --commit={COMMIT_HASH} || true ``` Ploi replaces `{COMMIT_HASH}` with the deployed commit before the script runs. With zero-downtime deployment, Ploi runs the deploy script before it switches the `current` link to the new release, so the marker can land a few seconds early. ## Envoyer Add a deployment hook (**Deployment Hooks**) that runs after **Activate New Release**, as the user that owns the app: ```bash cd "{{release}}" && php artisan lampwick:deploy --commit={{sha}} || true ``` Envoyer cancels a deployment when a hook fails; `|| true` stops a failed request from doing that. `{{release}}` and `{{sha}}` are [Envoyer's hook variables](https://docs.envoyer.io/projects/deployment-hooks) (checked 10 Nov 2026). ## Vercel Vercel has no deploy script to add a step to. Its GitHub integration reports each deployment to GitHub, so a workflow on `deployment_status` can record the deploy once Vercel says it's live: ```yaml # .github/workflows/lampwick-deploy.yml name: Lampwick deploy on: deployment_status permissions: {} jobs: record: if: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Production' runs-on: ubuntu-latest steps: - name: Record deploy in Lampwick env: LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }} run: | body=$(jq -n --arg version "${GITHUB_SHA::12}" --arg commit_sha "$GITHUB_SHA" \ --arg repo_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ --arg author "$GITHUB_ACTOR" --arg environment production '$ARGS.named') printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" | curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \ -H "Content-Type: application/json" --data "$body" \ || echo "::warning::Lampwick: deploy marker not recorded" ``` The job doesn't check out or run your code, and `permissions: {}` leaves it no access to the repository; keep it that way. Leave Vercel's fork protection on: every deployment, also one of a pull request from a fork, triggers this workflow with your secrets. The workflow file must be on the default branch. Drop the environment check to record preview deployments too, but send those to a separate project. On `deployment_status`, `GITHUB_SHA` is the deployed commit. Deploying with `vercel deploy --prod` from your own CI instead? Add the step after that, as in [GitHub Actions](#github-actions). ## Any other CI GitLab CI, Bitbucket Pipelines, CircleCI, Buildkite or a shell script: store the token as a secret variable `LAMPWICK_TOKEN` and run this after the deploy: ```bash body=$(jq -n --arg version "$VERSION" --arg commit_sha "$COMMIT_SHA" \ '{version: $version, commit_sha: $commit_sha, environment: "production"}') printf 'header = "Authorization: Bearer %s"\n' "$LAMPWICK_TOKEN" | curl -fsS --max-time 30 -K - -X POST https://app.lampwick.dev/api/v1/deploys \ -H "Content-Type: application/json" --data "$body" \ || echo "Lampwick: deploy marker not recorded" ``` Set `VERSION` and `COMMIT_SHA` from your CI's variables, for example `$CI_COMMIT_SHA` on GitLab or `$BITBUCKET_COMMIT` on Bitbucket. `jq` builds the JSON, so a version with quotes can't break it. The token goes to curl on stdin (`-K -`), not on its command line, where other processes on the machine could read it. The `|| echo` keeps a failed request from failing the pipeline; leave it out if a missing marker should stop the pipeline. The answer is `201` with the deploy: ```json {"deploy":{"author":null,"commit_sha":"9f1c2ab3d4e5f60718293a4b5c6d7e8f90a1b2c3","environment":"production","id":3,"new_issues":0,"source":"ingest_token","version":"v1.4.2", …}} ``` ## What to send | Field | | What to put in it | |---|---|---| | `version` | required | What's live now, at most 100 characters: a tag (`v1.4.2`), a release name or the commit. Use the same value you set as `LAMPWICK_DEPLOY`, if any. | | `commit_sha` | optional | The deployed commit, hex. | | `repo_url` | optional | The repository's `https://` URL. Lampwick links the commit as `{repo_url}/commit/{commit_sha}`. | | `author` | optional | Who deployed. | | `environment` | optional | `production`, `staging`, … | | `deployed_at` | optional | RFC 3339 time it went live. Default: now. Never later than now. | Send it when the new release is live, not when the build starts. A deploy stays live until the next one, and issues belong to the deploy that was live when they were first seen. Staging and production in one project mix up: use a project per environment. ## Let your agent add it Paste this into Claude Code, Cursor or Codex in your repository: ```text Add a Lampwick deploy marker to this project's deploy. Docs: https://lampwick.dev/docs/deploys.md 1. Find where the app is deployed: a GitHub Actions workflow, a Forge/Ploi/Envoyer deploy script in the repo or its README, vercel.json, or another CI config. If you can't tell, ask me. 2. Add the matching recipe from the docs as the last step, after the release is live. Use the project's ingest token from the LAMPWICK_TOKEN secret or the server's .env; never write the token into a file in the repository. 3. A failed marker must never fail the deploy (keep the recipe's `|| true` or `|| echo`). 4. Tell me which secret I need to add and where, and show me the diff. ``` ## Check that it works Run a deploy, then open the project's **Deploys** page in Lampwick. The deploy shows its version, commit and **from CI**. The charts get a marker at that time. Per deploy, the **Deploys** page also counts its new issues and the users affected: the signed-in users who hit an issue that started with it ([impact of an issue](/docs/users/#impact-of-an-issue)). Funnels show **Conversion by deploy**: users grouped by the deploy that was live when they entered ([read a funnel](/docs/funnels/#read-a-funnel)). ## Troubleshooting | Message | Cause | Fix | |---|---|---| | `401` *send the project's ingest token* | The token is a personal API token, or empty. | Use the ingest token from the project's **Settings** page. | | `400` *commit_sha must be a hexadecimal commit hash (4 to 64 characters)* | A tag or branch name in `commit_sha`. | Put the tag in `version` and the hash in `commit_sha`. | | `400` *version is required* | `version` is empty, for example an unset CI variable. | Check the variable you send as `version`. | | `429` *at most 20 deploys sent with the ingest token per project per hour* | More than 20 deploys in an hour, often a workflow that runs on every push to every branch. | Record only production deploys. | | `Could not record the deploy: HTTP 401` | `lampwick:deploy` sent a token that isn't the project's ingest token. | Copy the ingest token into `.env` again. | | `LAMPWICK_TOKEN is not set.` | `lampwick:deploy` runs where `.env` has no token. | Add `LAMPWICK_TOKEN` to the site's `.env`. | Next: [work with issues](/docs/issues/), including the list of issues new since the last deploy. --- Source: https://lampwick.dev/docs/opentelemetry/ # Send OpenTelemetry traces and logs > Point any OpenTelemetry SDK at https://app.lampwick.dev over OTLP/HTTP with your ingest token. Server spans become requests, errors become issues. Lampwick takes OpenTelemetry traces and logs over OTLP/HTTP. Any app with an OpenTelemetry SDK can send them by setting a few environment variables, with no Lampwick package. We tested these setups end to end: | Stack | Setup | Requests | Errors → issues | Queries | Logs | |---|---|---|---|---|---| | [Node.js (Express)](/docs/node/) | Zero-code: `--require` the auto-instrumentations | yes | yes, with stack trace | yes (`pg`) | via pino, winston or bunyan | | [Next.js](/docs/nextjs/) | `instrumentation.ts` with `@vercel/otel` | yes | yes, stack trace points into `.next/` | with `PgInstrumentation` | not captured | | [Python (FastAPI, Django)](/docs/python/) | Zero-code: `opentelemetry-instrument` | yes | yes, with stack trace | yes (`sqlite3`, Django ORM) | yes, `logging` | Go, Ruby, .NET and Java should work the same way, because the endpoint only speaks the protocol, but we haven't tested them yet. Ruby's SDK needs Ruby 3.3 or later. ## Requirements - The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it. - An OpenTelemetry SDK that exports OTLP over HTTP. Lampwick doesn't accept gRPC. ## Set the environment variables Set these wherever your app's environment lives (`.env`, your process manager, your host's dashboard): ```dotenv OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN OTEL_SERVICE_NAME=my-app OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=otlp ``` `YOUR_PROJECT_TOKEN` is the project's ingest token. The SDK adds `/v1/traces` and `/v1/logs` to the endpoint itself. `Authorization=Bearer%20YOUR_PROJECT_TOKEN` works as the header too. `OTEL_METRICS_EXPORTER=none` matters: Lampwick doesn't take metrics, and without it most SDKs try to send them every minute and log an error each time. ## What shows up where | OpenTelemetry | In Lampwick | |---|---| | Server span (`SpanKind.SERVER`) | **Requests**, one row per method and `http.route` | | Database span (`db.system`) | **Queries**, one row per statement; linked to the request it ran in | | Client span with an HTTP method | **Outgoing requests**, one row per host | | Consumer / producer span | **Jobs** / **Queued jobs** | | Root internal span | **Commands** | | `exception` span event | **Issues**, grouped by type and the top 3 frames of your own code | | Log record | **Logs**, linked to the request it was written in | | Log record with `exception.*` attributes | **Logs** and **Issues** (handled) | | `enduser.id` or `user.id` attribute | **Users** | Lampwick doesn't store middleware, router and other internal child spans, and doesn't count them as events. Connection spans such as `pg.connect` aren't queries and are left out too. An error is one occurrence per trace. SDKs often record the same error on several spans (Express 5 records it 3 times), and frameworks also log it (Django's `django.request` logger), so Lampwick keeps the first copy. The issue counts as unhandled when any copy came from a failed span. ## Check that it works 1. Start your app with the variables set and open a few pages. 2. Spans arrive within about 5 seconds: the SDKs export in batches. Open **Requests** in the project. 3. Lampwick answers each export with `200`. A `401` means the token is wrong, a `415` that the SDK sends gRPC or another content type. ## Limits - Protobuf (`application/x-protobuf`) or JSON (`application/json`), optionally gzip-compressed. - No gRPC and no metrics: `/v1/metrics` answers `404`. - Spans and log records count as events like everything else; spans Lampwick doesn't store don't count. - Stack traces are parsed from `exception.stacktrace` for Node, Python, JVM, .NET, Go, Ruby and PHP. Source maps aren't applied, so bundled code shows bundle file names. ## Troubleshooting | What you see | Cause and fix | |---|---| | Nothing arrives, and the app logs `401` | The token in `OTEL_EXPORTER_OTLP_HEADERS` is wrong or has spaces around it. Copy it again from **Settings**. | | Nothing arrives, and the app logs `415` or a connection error on port 4317 | The SDK uses gRPC. Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`. | | The app logs metric export errors | Set `OTEL_METRICS_EXPORTER=none`. | | Requests show up, logs don't | The language's logging bridge isn't on; see the page for your stack. | | Requests show the URL but no route | The framework instrumentation isn't loaded, so only the HTTP server span exists without `http.route`. | --- Source: https://lampwick.dev/docs/node/ # Monitor a Node.js app with OpenTelemetry > Install the OpenTelemetry auto-instrumentations, set the OTLP environment variables and start Node with --require. Express requests and errors reach Lampwick. The OpenTelemetry auto-instrumentations for Node.js send requests, database queries, outgoing calls and errors to Lampwick without code changes. Logs come from pino, winston or bunyan. This page is the setup we ran against Lampwick; the general reference is [OpenTelemetry](/docs/opentelemetry/). Tested with Node.js 26.9, `@opentelemetry/auto-instrumentations-node` 0.83.0 (`@opentelemetry/sdk-node` 0.301.1), Express 5.3, `pg` 8.23 and pino 10.4, on 10 November 2026. ## Requirements - Node.js 22.15 or later (what the current OpenTelemetry packages require). - The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it. ## Let your agent do it Paste this into your coding agent, in the app's repository: ```text Add OpenTelemetry to this Node.js app so it reports to Lampwick: follow https://lampwick.dev/docs/node.md exactly (zero-code setup, environment variables, no code changes besides a pino logger if the app has no logger). Read the ingest token from the environment, never commit it. ``` ## Install the packages ```bash npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node ``` ## Set the environment variables ```bash export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN export OTEL_SERVICE_NAME=my-app export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=otlp export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" ``` `YOUR_PROJECT_TOKEN` is the project's ingest token. `NODE_OPTIONS` loads the instrumentation before your app's own code, which it needs to patch `http`, `express` and `pg`. You can also put the `--require` in your start command instead: `node --require @opentelemetry/auto-instrumentations-node/register app.js`. ## The app we tested Nothing in it is specific to OpenTelemetry or Lampwick: ```js const express = require('express'); const pino = require('pino'); const { Pool } = require('pg'); const logger = pino(); const app = express(); const db = new Pool({ connectionString: process.env.DATABASE_URL }); app.get('/orders/:id', async (req, res) => { const { rows } = await db.query('SELECT id, total FROM orders WHERE id = $1', [req.params.id]); logger.info({ orderId: req.params.id }, 'order looked up'); res.json(rows[0] ?? null); }); app.get('/boom', () => { throw new Error('Checkout failed for order 42'); }); app.listen(3000, () => logger.info('listening on :3000')); ``` ## Check that it works Start the app and open `/orders/1`, `/orders/2` and `/boom`. Within about 5 seconds the project shows: - **Requests**: `GET /orders/:id` (2) and `GET /boom` (1, status 500). - **Queries**: `SELECT id, total FROM orders WHERE id = $1`, linked to its request. - **Issues**: `Error: Checkout failed for order 42`, unhandled, at `app.js:16`, with the stack trace. One occurrence, although Express 5 records the error on 3 spans. - **Logs**: `listening on :3000` and 2 × `order looked up`; the last two are linked to their request. ## What's missing - **`console.log` isn't sent.** The instrumentations only capture pino, winston and bunyan. Use one of those for logs you want in Lampwick. - **Errors you catch yourself** don't become issues unless you record them: `trace.getActiveSpan()?.recordException(err)` with `trace` from `@opentelemetry/api`. - **Bundled or transpiled code**: stack traces point at the files Node runs. Source maps aren't applied. - **Users**: set `enduser.id` on the active span (`trace.getActiveSpan()?.setAttribute('enduser.id', user.id)`) to see them under **Users**. ## Troubleshooting | What you see | Cause and fix | |---|---| | Requests have no route (`GET` only) | The instrumentation loaded after `express`. Use `NODE_OPTIONS` or `--require`, not an `import` halfway through your app. | | `OTLPExporterError: Unauthorized` | The token is wrong. Copy it again from **Settings**. | | Metric export errors every minute | `OTEL_METRICS_EXPORTER=none` isn't set in the app's environment. | --- Source: https://lampwick.dev/docs/nextjs/ # Monitor a Next.js app with OpenTelemetry > Add instrumentation.ts with @vercel/otel and set the OTLP environment variables. Next.js App Router requests and server errors show up in Lampwick. Next.js has OpenTelemetry built in: with an `instrumentation.ts` file that calls `@vercel/otel`, every request and every server-side error is exported. Point it at Lampwick with environment variables. The general reference is [OpenTelemetry](/docs/opentelemetry/). Tested with Next.js 16.4 (App Router, `next build` and `next start`), `@vercel/otel` 2.3.0, `@opentelemetry/instrumentation-pg` 0.76.0 and Node.js 26.9, on 10 November 2026. ## Requirements - Next.js 15 or later (`instrumentation.ts` needs no config flag from 15 on). We tested 16.4. - The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it. ## Let your agent do it Paste this into your coding agent, in the app's repository: ```text Add OpenTelemetry to this Next.js app so it reports to Lampwick: follow https://lampwick.dev/docs/nextjs.md exactly (instrumentation.ts with @vercel/otel, the environment variables, PgInstrumentation only if the app uses pg). Read the ingest token from the environment, never commit it. ``` ## Install the packages ```bash npm install @vercel/otel @opentelemetry/api @opentelemetry/api-logs @opentelemetry/instrumentation @opentelemetry/sdk-logs ``` ## Add instrumentation.ts Create `instrumentation.ts` in the project root (next to `app/`, or in `src/` if your app lives there): ```ts import { registerOTel } from '@vercel/otel'; export function register() { registerOTel({ serviceName: 'my-app' }); } ``` ## Set the environment variables ```dotenv OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN OTEL_METRICS_EXPORTER=none ``` `YOUR_PROJECT_TOKEN` is the project's ingest token. On Vercel, add them under **Settings → Environment Variables**; locally, in `.env.local`. ## Database queries `@vercel/otel` instruments `fetch` only. For `pg`, add its instrumentation. The current `@opentelemetry/instrumentation-pg` needs a newer `@opentelemetry/api` than `@vercel/otel` 2.3 allows, so install 0.76.0: ```bash npm install @opentelemetry/instrumentation-pg@0.76.0 ``` ```ts import { registerOTel } from '@vercel/otel'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; export function register() { registerOTel({ serviceName: 'my-app', instrumentations: ['auto', new PgInstrumentation()], }); } ``` ## The app we tested A route handler with a query, and a page that throws: ```ts // app/orders/[id]/route.ts import { Pool } from 'pg'; const db = new Pool({ connectionString: process.env.DATABASE_URL }); export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; const { rows } = await db.query('SELECT id, total FROM orders WHERE id = $1', [id]); return Response.json(rows[0] ?? null); } ``` ```tsx // app/boom/page.tsx export const dynamic = 'force-dynamic'; export default async function Boom() { throw new Error('Checkout failed for order 42'); } ``` ## Check that it works Run `next build` and `next start`, then open `/orders/1`, `/orders/2` and `/boom`. Within about 5 seconds the project shows: - **Requests**: `GET /orders/[id]` (2) and `GET /boom` (1, status 500). - **Queries**: `SELECT id, total FROM orders WHERE id = $1`, linked to its request (with `PgInstrumentation`). - **Issues**: `Error: Checkout failed for order 42`, unhandled, in the `/boom` request. Next.js also traces its own startup and module loading. Lampwick leaves those spans out. ## What's missing - **Stack traces point into `.next/server/chunks/`.** Next.js bundles server code and Lampwick doesn't apply source maps, so the issue names the chunk file and function names may be minified. The message and the request it happened in are exact. - **Logs aren't sent.** `console.log` isn't captured, and `@vercel/otel` exports no logs by default. We haven't tested a logging setup with Next.js yet. - **Client-side errors** happen in the browser, not on the server. Add the [browser errors snippet](/docs/browser/) for those. - **Edge runtime**: we tested the Node.js runtime only. ## Troubleshooting | What you see | Cause and fix | |---|---| | Nothing arrives, and nothing is logged | Next.js only calls `register()` from `instrumentation.ts` in the project root, or in `src/` when the app lives there. Move the file. A `401` or another export error: see the [OpenTelemetry troubleshooting](/docs/opentelemetry/#troubleshooting). | | `npm install` fails with `ERESOLVE` on `@opentelemetry/api` | A current OpenTelemetry instrumentation package wants a newer API than `@vercel/otel` allows. Pin a version built on `@opentelemetry/instrumentation` 0.2xx, such as `@opentelemetry/instrumentation-pg@0.76.0`. | | `Specified module format (CommonJs) is not matching` during `next build` | `package.json` has `"type": "commonjs"` (what `npm init -y` writes). Remove the line. | | Requests arrive, queries don't | Add `PgInstrumentation` (or the instrumentation for your database client) as above. | --- Source: https://lampwick.dev/docs/python/ # Monitor a Python app with OpenTelemetry > Install opentelemetry-distro and start FastAPI or Django with opentelemetry-instrument. Requests, queries, errors and logs reach Lampwick, no code changes. OpenTelemetry's zero-code instrumentation for Python sends requests, database queries, errors and `logging` records from a FastAPI or Django app to Lampwick. You start the app with `opentelemetry-instrument` and set environment variables; the code stays as it is. The general reference is [OpenTelemetry](/docs/opentelemetry/). Tested with Python 3.9.6, `opentelemetry-distro` 0.62b1 (`opentelemetry-sdk` and `opentelemetry-exporter-otlp-proto-http` 1.41.1), FastAPI 0.128 on uvicorn 0.39, and Django 4.2 with SQLite, on 10 November 2026. ## Requirements - Python 3.9 or later. - The project's ingest token, from the **Connect your app** card on the project's **Settings** page. Only team owners see it. ## Let your agent do it Paste this into your coding agent, in the app's repository: ```text Add OpenTelemetry to this Python app so it reports to Lampwick: follow https://lampwick.dev/docs/python.md exactly (opentelemetry-distro, opentelemetry-bootstrap, opentelemetry-instrument, the environment variables). Read the ingest token from the environment, never commit it. ``` ## Install the packages In the app's virtualenv, after your app's own dependencies: ```bash pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http opentelemetry-bootstrap -a install ``` `opentelemetry-bootstrap` looks at what's installed and adds the matching instrumentations: for our FastAPI app `opentelemetry-instrumentation-fastapi`, `-asgi`, `-sqlite3` and `-logging`, for Django `opentelemetry-instrumentation-django`. Run it again after you add a framework or database driver. To pin them, `opentelemetry-bootstrap -a requirements` prints the list for `requirements.txt`. ## Set the environment variables ```bash export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN export OTEL_SERVICE_NAME=my-app export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=otlp ``` `YOUR_PROJECT_TOKEN` is the project's ingest token. `OTEL_LOGS_EXPORTER=otlp` is all logs need: the `logging` instrumentation sends every record that passes your loggers' level. ## FastAPI Start uvicorn through `opentelemetry-instrument`: ```bash opentelemetry-instrument uvicorn main:app --port 8000 ``` The app we tested: ```python import logging import sqlite3 from fastapi import FastAPI logging.basicConfig(level=logging.INFO) logger = logging.getLogger("shop") app = FastAPI() db = sqlite3.connect("shop.db", check_same_thread=False) db.execute("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, total INTEGER)") db.execute("INSERT OR IGNORE INTO orders VALUES (1, 10), (2, 20)") db.commit() @app.get("/orders/{order_id}") def show_order(order_id: int): cursor = db.cursor() cursor.execute("SELECT id, total FROM orders WHERE id = ?", (order_id,)) row = cursor.fetchone() logger.info("order %s looked up", order_id) return {"id": row[0], "total": row[1]} if row else None @app.get("/boom") def boom(): raise RuntimeError("Checkout failed for order 42") @app.get("/handled") def handled(): try: 1 / 0 except ZeroDivisionError: logger.exception("could not compute the discount") return {"ok": True} ``` After opening `/orders/1`, `/orders/2`, `/boom` and `/handled`, the project shows: - **Requests**: `GET /orders/{order_id}` (2), `GET /boom` (status 500) and `GET /handled`. - **Queries**: `SELECT id, total FROM orders WHERE id = ?`, linked to its request. - **Issues**: `RuntimeError: Checkout failed for order 42`, unhandled, at `main.py:27`; and `ZeroDivisionError: division by zero`, handled, from `logger.exception`. - **Logs**: 2 × `order … looked up` (info) and `could not compute the discount` (error), each linked to its request. ## Django Django's instrumentation needs `DJANGO_SETTINGS_MODULE` in the environment, and the development server's autoreloader has to be off: ```bash export DJANGO_SETTINGS_MODULE=mysite.settings opentelemetry-instrument python manage.py runserver --noreload ``` With views that run a query (through `django.db.connection` or the ORM), log with `logging` and raise an exception, the project shows: - **Requests**: `GET /orders/` and `GET /boom` (status 500), named after the URL pattern. - **Queries**: every SQL statement, linked to its request, plus the queries Django runs at startup (without a request). - **Issues**: `RuntimeError: Checkout failed for order 42`, unhandled, once per request. Django also logs it as `Internal Server Error: /boom` on the `django.request` logger; that line shows under **Logs**, and Lampwick counts the error once. We tested `runserver`. With gunicorn or uWSGI, the worker processes fork after the SDK starts; the OpenTelemetry docs describe a `post_fork` hook for that, which we haven't tested. ## What's missing - **`connection.execute()` on `sqlite3` isn't traced**, only `cursor.execute()`. Django and SQLAlchemy use cursors, so this only affects raw `sqlite3` code. - **Info logs need a level.** Python's root logger drops records below `WARNING` unless you configure it (`logging.basicConfig(level=logging.INFO)`, or `LOGGING` in Django). - **`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`** is the old switch. It still works, but logs a deprecation warning into your logs; leave it out. ## Troubleshooting | What you see | Cause and fix | |---|---| | Django requests don't arrive, other spans do | `DJANGO_SETTINGS_MODULE` isn't set in the environment `opentelemetry-instrument` runs in, or the autoreloader is on: add `--noreload`. | | `Skipping installation of LoggingHandler … to avoid duplicate logs` in your logs | `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true` is set. Remove it. | | `NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+` | macOS's system Python is built with LibreSSL. The export still works; use a Python from python.org or Homebrew to get rid of it. | | Queries are missing | The driver's instrumentation isn't installed. Run `opentelemetry-bootstrap -a install` again after installing the driver. | --- Source: https://lampwick.dev/docs/issues/ # Error grouping: work with issues > Lampwick groups exceptions into issues by class, code, file and line. Resolve, ignore or reopen them; a resolved issue reopens when it happens again. An issue is one kind of exception in your app: Lampwick groups every occurrence by class, code, file and line, so a thousand identical errors are one row with a count. ## Requirements - A project that receives exceptions, from the [Laravel package](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/). - Over HTTP ingest, an `exception` record needs a `_group` key; records with the same `_group` are one issue. ## Find an issue 1. Open the project and go to **Issues** in the sidebar. 2. Pick a tab: **Open**, **Resolved**, **Ignored** or **All**. 3. Optional: search by class, message or file (/) and pick a time range. Each row shows the issue number (`#12`), class, message, file and line, an **Unhandled** badge when nothing caught it, the count in the range, users affected, the total, and last and first seen. ## Read an issue The issue's page has the stack trace with source code, the occurrences chart, the users affected, **Recent occurrences**, and the context of one occurrence: **Server**, **Deploy**, **User** and **During** (the request or job, linked to its timeline). When the exceptions carry a user, the page also shows **Impact on users**: how many users the issue hit, how many came back within 7 days, and who hasn't been seen since. See [impact of an issue](/docs/users/#impact-of-an-issue). To let an AI agent read the stack trace and propose a fix, see [debug Laravel production errors with Claude Code](/guides/debug-laravel-production-errors-with-claude-code/). ## Resolve, ignore or reopen On the issue's page, or with ⌘ K → **Resolve issue**, **Ignore issue** or **Reopen issue**: | Status | What happens on a new occurrence | |---|---| | Open | It counts. | | Resolved | The issue reopens, and the team gets a push notification about the regression. | | Ignored | It counts, but the issue stays ignored and sends nothing. | In the API: `updateIssue` with `{"status": "resolved"}`, `"ignored"` or `"open"`. ## Notifications The project's team gets a push notification in the Lampwick iOS app for every new issue and every resolved issue that comes back, and so do the team's [alert channels](/docs/alerts/#alert-channels) (Slack, Discord, Microsoft Teams, webhooks). Issues send no email; email goes with the other [alerts](/docs/alerts/). ## Retention Occurrences follow the retention of your other records. The issue itself is deleted 90 days after it was last seen, or after the retention when that is longer. ## Troubleshooting | What you see | Cause and fix | |---|---| | An exception sent over HTTP doesn't show under **Issues** | The record has no `_group`. Add one, for example a hash of the class, file and line. | | One error shows up as several issues | The class, code, file or line differs between them, for example after a deploy moved the line. | | An ignored issue still counts up | Ignoring stops notifications, not counting. | To let an AI agent read and resolve issues for you, [connect it to the MCP server](/docs/mcp/). --- Source: https://lampwick.dev/docs/users/ # Users, active users and retention > See how many signed-in users use your app each day, week and month, how many come back, and which errors drive them away. From the data you already send. Lampwick counts the signed-in users of your app from the requests and exceptions it already receives: who was active on which day, how many came back the weeks after, and whether an error made them leave. There is nothing to install beyond the integration you already have. ## Requirements - Your app sends the signed-in user with its requests: - [OpenTelemetry](/docs/opentelemetry/) (Node, Python, Go, Java, .NET, Ruby): set `enduser.id` on the request span, as in the [Node.js guide](/docs/node/). - [HTTP ingest](/docs/http-ingest/): send `user` on each record. - [Laravel](/docs/laravel/): the `lampwick/laravel` package sends the authenticated user for you. - Any plan. How far back it goes depends on the plan (see [How long it is kept](#how-long-it-is-kept)). ## Active users Open **Users** in the sidebar and choose **Active users**. - **Today**, **Last 7 days** and **Last 30 days** count the users active in that window (DAU, WAU and MAU). - **Stickiness** is the average number of daily users over the last 30 full days, divided by the users active in those days. 20 % means a typical user comes on 6 of 30 days. It shows once Lampwick has 30 full days of activity. - The chart shows each day (30d, 90d) or week (12m) split into **New** users, first seen that day or week, and **Returning** users. Deploys show as dotted lines, so a drop after a release stands out. A user is active on a day when your app sent a request or an exception of theirs that day. Days are UTC days. The **Overview** shows the users active in the last 30 days next to the users of the selected range. ## Retention **Users → Retention** groups users by when Lampwick first saw them, and shows for each group the share active in every period after. Choose the period in the menu at the top: | Period | Rows | Columns | |---|---|---| | **Daily** | the last 30 days | day 0 to 13 | | **Weekly** (default) | the last 12 weeks, Monday to Sunday | week 0 to 11 | | **Monthly** | the last 12 calendar months | month 0 to 11 | All periods are UTC. Only periods since the kept data starts get a row, so how far back the table goes depends on your plan (see [How long it is kept](#how-long-it-is-kept)): on Free, monthly cohorts cover one or two months. - **On the day**, **In the week** or **In the month** counts users active in that period. Use it to see how often people come back. - **In it or later** counts users active in that period or any later one. Use it to see how many are still around at all. - The route menu, at the top right, limits the table to users who requested one route, such as `POST /checkout`. It lists the routes of the project by traffic of the last 7 days, like the step picker of a [funnel](/docs/funnels/), without health checks. It looks at the newest 200,000 requests of that route within your plan's record retention, so on Free (3 days of records) it finds only recent users of the route. The current period isn't over yet: each row's last value is striped and can still grow. Rows of fewer than 10 users are faded, because one user moves their shares a lot. A user's first period is the first one Lampwick still holds. In the period the kept data starts (marked `*`), users from before count as new. ## Export as CSV **Export CSV**, at the top right of **Users** and **Users → Retention** (on a computer), downloads what the page shows as a CSV file for a spreadsheet: - **Users**: up to 1,000 users of the range (and search), most active first, with their requests, server errors, exceptions, request p95 in milliseconds, first and last seen. - **Retention**: one row per cohort with its first and last day, its size, and per period the share active and the number of users active, for the period and counting you chose. In every export, shares are percentages from 0 to 100 with one decimal (columns ending in `_pct`), durations are rounded (`_ms`, or whole seconds in `_secs`) and times are UTC. Names, usernames and routes come from your app. A value that starts with `=`, `+`, `-` or `@`, or has one of them right after a comma, semicolon or tab, gets a `'` in front, so a spreadsheet shows it as text instead of running it as a formula. ## Impact of an issue An issue's page shows **Impact on users** when the exceptions carry a user: - **came back**: of the users who first hit the issue more than 7 days ago, how many were active again within 7 days. Next to it, the usual share for your project: for each user active in the 30 days before last week, the share of their active days followed by another visit within 7 days, averaged over those users. Every user counts once, so people who use the app daily don't pull it up. On big projects it comes from a fixed sample of about 50,000 users. When the issue's share is more than 15 points lower, it shows in red: the error may drive people away. With fewer than 10 such users it shows counts ("4 of 6 came back") instead of a percentage. - **Not seen since**: users with no request or job since their last hit, most recent first, linked to their user page. Under the occurrence, **Before the error** lists that user's last 20 requests and jobs up to the exception, with status and duration. The request it happened in is highlighted. The **Deploys** page shows per deploy how many users hit an issue that started with it. ## Erase a user's data For a data subject request (GDPR article 17), open the user's page (**Users →** the user) and choose **Erase this user's data** at the bottom of the page. Lampwick then: 1. deletes their name and username, their activity (active days and counts) and their attempts at your [funnels](/docs/funnels/); 2. removes the link to them from their requests and exceptions, with the `user`, `ip`, `headers` and `payload` (request body) fields of those records. The records themselves stay, anonymous, so request and error charts don't change; active users, retention and funnel steps no longer count the user. The funnels' daily totals and the user paths hold no user ids and stay as they are. Lampwick doesn't search free text such as exception messages, log lines or URLs for the user. If your app keeps sending data about them, it is stored again. The team's audit log records the erasure with a keyed hash instead of the user's id. With the API: `DELETE /api/v1/projects/{pid}/users/{uid}` (see the [API docs](/docs/api/)). ## IP addresses By default Lampwick stores client IP addresses as your app sends them. To store less, open the project's **Settings → Privacy** and choose **Shorten them before storing**: IPv4 addresses keep their first three parts (`203.0.113.77` becomes `203.0.113.0`) and IPv6 addresses their first 48 bits. It applies to the `ip` of requests and OpenTelemetry spans and to addresses in request headers (`X-Forwarded-For`, `Forwarded` and the like), for records stored from then on. ## How long it is kept Activity per user (which days, and counts per month) is kept longer than records, because it holds no details of what they did: | Plan | Records | User activity | |---|---|---| | Free | 3 days | 30 days | | Pro | 14 days | 90 days | | Team | 30 days | 13 months | | Business | 60 days | 13 months | ## Check that it works Open **Users → Active users**. If it says "No signed-in users yet", open a request on a **Requests** page and look for a user in it. No user there means your app doesn't send one: check the requirements above. The numbers include the current hour. Activity is added to the long-term store once an hour; requests that reach Lampwick more than an hour after they happened still show on the request pages, but not in these numbers. ## API and MCP `getUserActivity`, `getRetention`, `getIssueImpact` and `getUser` (its `activity`) give the same numbers, and are tools of the [MCP server](/docs/mcp/). `getRetention` takes `granularity` (`day`, `week` or `month`; `week` by default). `listUsers` and `getRetention` take `format=csv` for the CSV export. Erasing a user is in the API only, not an MCP tool. --- Source: https://lampwick.dev/docs/uptime/ # Uptime monitoring: know when a URL is down > An uptime monitor requests a URL every 30 seconds to 1 hour. 2xx and 3xx are up; two failed checks in a row are down and alert the team. An uptime monitor requests one public URL on a fixed interval and alerts the team when it stops answering. It needs no code in your app. For what else takes an app offline (certificates, domains, DNS, broken links), see [how to know when your app is down](/guides/know-when-your-app-is-down/). ## Requirements - A project. Every team member can manage its monitors. - A public `http://` or `https://` URL of at most 2,000 characters, without a username or password. Loopback, private and other non-public addresses are refused. - At most 100 monitors per project, 100 per team, and 300 over all the teams one owner owns. ## Add a monitor 1. Open the project and go to **Uptime** in the sidebar, or press ⌘ K → **Add uptime monitor**. 2. Under **Add monitor**, enter the **URL**, for example `https://example.com/up`. 3. Optional: a **Name** (default: the host). 4. Pick **Check every**: 30 seconds, 1 minute (the default), 5 minutes, 10 minutes, 30 minutes or 1 hour. 5. Click **Add monitor**. The first check runs within seconds. Each monitor also checks its [SSL certificate, domain and DNS](/docs/host-checks/). ## Check that it works The monitor's page shows **Status** (**Pending** until the first check, then **Up** or **Down**), the uptime in the selected range, the average and p95 response time, and the latest 20 checks with their HTTP status, duration and error (**Show all** lists the latest 100). ## How a check decides up or down | Rule | Value | |---|---| | Up | The final response, after redirects, has status 2xx or 3xx. | | Failed check | Any other status, no response headers within 10 seconds, a DNS, connection or TLS error, or more than 5 redirects. | | Down | 2 failed checks in a row. | | Retry | After a first failure, Lampwick checks again within 15 seconds instead of waiting a full interval. | | Back up | 1 successful check. | | Method | `GET` | | User-Agent | `lampwick-uptime/` | A single dropped request alerts no one; an outage is noticed within one interval plus about 15 seconds. ## Pick the right URL Monitor a health endpoint, such as Laravel's `/up` route, rather than a heavy page. If a firewall, WAF or bot protection (Cloudflare, for example) blocks automated requests, allow the User-Agent `lampwick-uptime`, or every check fails with `HTTP 403`. ## Change, pause or delete a monitor On the monitor's page: - **Settings → Monitor**: change the URL, name or interval. A new URL starts over: status **Pending**, checked at once. - **Alerts → Mute alerts for everyone**: the checks keep running, but no one gets [alerts](/docs/alerts/) for this monitor. - **Pause checks**, at the bottom of the page: no checks, host checks or crawl. **Resume checks** starts over like a new URL. - **Delete monitor**, next to it: deletes the monitor and its check history. Check history follows the retention of your other records. ## API | Operation | What it does | |---|---| | `listMonitors` | Monitors with uptime, response times and latest checks. | | `createMonitor` | Body: `url`, `name`, `interval_secs` (30 to 3600, any value). | | `getMonitor` | One monitor with its checks, DNS changes and charts. | | `updateMonitor` | `url`, `name`, `interval_secs`, `paused`, `notify`, and the host check switches. | | `deleteMonitor` | Deletes the monitor and its checks. | Tokens and conventions: [API](/docs/api/). ## Troubleshooting | What you see | Cause and fix | |---|---| | `HTTP 403 Forbidden` on every check, while the site works in your browser | A WAF or bot protection blocks the check. Allow the User-Agent `lampwick-uptime`, or monitor a health endpoint that it doesn't filter. | | `timed out after 10 s` | The server took longer than 10 seconds to send response headers. Monitor a lighter endpoint, or find what makes it slow. | | `connection failed: …` | DNS, the connection or TLS failed; the rest of the message says why. An expired or invalid certificate shows here too. | | `more than 5 redirects` | The URL redirects in a loop or through too many hops. Monitor the final URL. | | `address is not public` | The URL points to a private or loopback address. Lampwick only checks public addresses. | | `this team already has the maximum of 100 uptime monitors` | Delete monitors you no longer need, in any project of the team. | To show your monitors to customers, [create a status page](/docs/status-pages/). --- Source: https://lampwick.dev/docs/host-checks/ # Check SSL certificates, domains and DNS > Every uptime monitor also checks its SSL certificate, domain expiry (RDAP) and NS/MX records, and alerts before expiry or when DNS changes. Every [uptime monitor](/docs/uptime/) also checks the SSL certificate, domain registration and DNS records of its URL. The team gets an alert before a certificate or domain expires, when a certificate becomes invalid and when DNS records change. ## Requirements - An uptime monitor. The three checks are on by default. - For the certificate check, an `https://` URL. ## Turn checks on or off 1. Open the monitor's page from **Uptime**. 2. Under **Certificate, domain & DNS**, tick or untick **SSL certificate**, **Domain expiry** and **DNS changes**. 3. Optional: tick **Also A, AAAA and CNAME records**. 4. Click **Save**. In the API: the `check_ssl`, `check_domain`, `check_dns` and `dns_addresses` fields of `updateMonitor`. ## Check that it works The **Health** row on the monitor's page has the tiles **SSL**, **Domain** and **DNS**, each **Pending** until its first check, within a minute. The cards below show the certificate, the domain's expiry date, and the DNS records with **Recent changes**. The monitor list shows a badge when a certificate is invalid (**SSL invalid**) or expires within 14 days (**SSL 9d**), or a domain expires within 30 days (**Domain 21d**). ## SSL certificate | | | |---|---| | How often | Every 6 to 12 hours, and within a minute after a new monitor or a new host. | | What it reads | Expiry date, issuer, names, and whether the certificate is valid for the host (Mozilla's root certificates). | | Expiry alerts | 14, 7, 3 and 1 days before it expires, each once per certificate. | | Invalid alert | When it expires, names the wrong host or has an unknown issuer. Once, until it is valid again. | A failed connection gives no certificate alert; the uptime check reports it. When a check fails because of the certificate, the monitor's down alert says why and no separate certificate alert is sent. ## Domain expiry Lampwick looks up the registrable domain of the URL once a day through RDAP, at the registry's server from the IANA bootstrap file. For `shop.example.com` that is `example.com`, for `x.github.io` it is `github.io`. Monitors on one domain share one lookup. | | | |---|---| | Expiry alerts | 30, 14, 7 and 1 days before the registration expires, once per threshold, expiry date and project. | | Registry without an expiry date | Some registries, including `.nl`, publish no expiry date or have no RDAP server. The tile says **Unknown**, no alerts are sent, and Lampwick tries again weekly. | | Failed lookups | Retried after 1, 2, 4 … hours, at most a day apart. | A URL with an IP address or an unknown suffix has no registrable domain; the tile says **None**. ## DNS changes | | | |---|---| | How often | Hourly. | | Records | NS and MX of the host and its domain. With **Also A, AAAA and CNAME records**, those too. | | Resolvers | Public resolvers (Cloudflare and Google), never your server's own. | | Confirmation | A difference is looked up again after 5 minutes. Only when the second lookup agrees does the change go into **Recent changes** and alert. | | Alert | **DNS records changed: HOST**, listing the added and removed records. | A, AAAA and CNAME are off by default because CDNs and load balancers rotate them. ## Paused and muted monitors A paused monitor gets no host checks. With **Mute alerts for everyone**, the checks run but no one gets [alerts](/docs/alerts/). ## Troubleshooting | What you see | Cause and fix | |---|---| | **SSL: None**, "Only https URLs have a certificate" | The monitor's URL is `http://`. Change it to `https://` to check the certificate. | | **SSL: Unknown**, "Could not read it: …" | The handshake failed. The uptime check shows whether the site is reachable. | | **Domain: Unknown**, "the .nl registry doesn't publish expiry dates" | Lampwick can't warn you before expiry. Turn on auto-renew at your registrar. | | **Domain: Unknown**, "the lookup failed; Lampwick retries it" | The RDAP server didn't answer or returned an error. Lampwick retries, at most a day apart. | | **DNS: None** | The URL is an IP address, or a name that public DNS can't resolve, such as `localhost` or `*.internal`. | | Frequent **DNS records changed** alerts | Your CDN or load balancer rotates the A, AAAA or CNAME records. Untick **Also A, AAAA and CNAME records**. | To find broken links on the same site, [turn on the site crawl](/docs/crawler/). --- Source: https://lampwick.dev/docs/crawler/ # Find broken links and mixed content > Turn on the site crawl of an uptime monitor. Once a day Lampwick follows up to 250 pages, checks every link and reports http:// resources on https pages. The site crawl follows the links of a site once a day, checks every link it finds, and lists `http://` resources on `https` pages (mixed content). You turn it on per [uptime monitor](/docs/uptime/); it is off by default. ## Requirements - An uptime monitor that isn't paused. - At most 10 monitors with the site crawl per team, and 20 over all the teams an owner owns. - If a firewall, WAF or bot protection filters crawlers, allow the User-Agent `lampwick-crawler`. ## Turn on the site crawl 1. Open the monitor's page from **Uptime**. 2. On the **Site crawl: broken links & mixed content** card, click **Turn on site crawl**. The first crawl starts within a minute or two. 3. If the monitor checks a health endpoint such as `/up`, open the results page (**Crawl settings** on the card, **View results** after the first crawl) and set **Start crawl at** to your homepage. Empty means the monitor URL. ## Check that it works After the crawl, the card shows **Last crawl** with the page count, the time it took and the counts of broken links and mixed content. **View results** opens the tabs **Broken links** and **Mixed content**, and **Recent crawls** (the latest 30). ## What counts as broken | Result | Rule | |---|---| | Broken | Status 4xx or 5xx (except 429), a DNS, connection, TLS or timeout error, or more than 5 redirects. | | **Needs login** | Status 401 or 403. Listed apart; not broken, no alert. | | New | Broken or mixed since the previous successful crawl. | Broken links are grouped by 5xx, 404, other 4xx and connection errors, with a filter for **Internal** and **External**. ## How the crawl works | | | |---|---| | Pages followed | `` links from the start URL to the same site: same scheme and port, `www.` and the bare domain as one host, judged from the start page's final URL after redirects. | | Links checked | Every link found, external too. External links get `HEAD`, or a `GET` of at most 16 KB when `HEAD` fails, on ports 80 and 443 only. | | Mixed content | Any `http://` subresource on an `https` page: `img` and `source` (with `srcset`), `script`, `link rel=stylesheet`, `iframe`, `video`, `audio`, `track`, `embed`, `object`, `url(...)` in an inline `style`. Listed once each, with up to 5 pages and the page count. | | robots.txt | The group `lampwick`, otherwise `*`. Unreachable (5xx or timeout) means nothing is crawled. A page with `` has its links skipped; `rel="nofollow"` links are checked, not followed. | | Requests | User-Agent `lampwick-crawler/ (+https://lampwick.dev)`, one at a time per site, 250 ms apart. | | Stored | URLs, status codes and Lampwick's own error text, never page content. Only the latest successful crawl's details are kept. | ## Limits per crawl | Limit | Value | |---|---| | Pages | 250 | | Depth | 10 clicks from the start page | | Size | 2 MB per page (only HTML is read), 50 MB in total | | External links | 500 links, 1,000 requests (every redirect counts), within 3 minutes | | Time | 10 seconds per request, 10 minutes in total | | Slow site | Stops after 5 pages when they take more than 2 seconds each on average | | **Crawl now** | Once per 10 minutes per monitor | A crawl that hits a limit says so, for example "Stopped early: the site has more than 250 pages". Turning the crawl on doesn't start the **Crawl now** wait. ## Ignore a link **Ignore** next to a link on the results page stops it counting and alerting; **Unignore** in the collapsed list brings it back. Up to 1,000 ignored links per monitor. ## Alerts After a crawl with new broken links or mixed content, the team gets an alert with counts only, no URLs: > **Broken links: Shop** > The crawl of shop.example.com found 12 broken links (2 new) on 183 pages. Open the results to see where. The first crawl reports everything it found. The same findings alert at most once per 30 days, and **Mute alerts for everyone** on the monitor mutes these [alerts](/docs/alerts/) too. ## API `getMonitorCrawl` (settings, latest crawl, results), `updateMonitorCrawl` (`enabled`, `start_url`), `startMonitorCrawl` (crawl now; `429` within 10 minutes of the last one), `ignoreCrawlLink` and `unignoreCrawlLink` (body: `url`). Tokens and conventions: [API](/docs/api/). ## Troubleshooting | What you see | Cause and fix | |---|---| | "Only the start page was crawled: it has no links to follow" | The monitor checks a health endpoint. Set **Start crawl at** to your homepage. | | The crawl fails with a refused, reset or timed-out connection | The site blocks crawlers. Allow the User-Agent `lampwick-crawler` in your firewall or WAF. | | The crawl fails on the certificate | Renew or fix the certificate, then use **Crawl now**. | | robots.txt doesn't allow the start page | Add `User-agent: lampwick` and `Allow: /` to your robots.txt. | | An external link is "not checked (non-standard port)" | Lampwick only checks external links on ports 80 and 443. | | "Stopped early: the site responds slowly" | Pages took more than 2 seconds on average. Check your server's response time. | --- Source: https://lampwick.dev/docs/cron-health/ # Set up schedule health > Lampwick learns each scheduled task's cron from its runs and alerts when a task misses a run, starts failing or runs longer than you allow. **Schedule health** shows when each scheduled task should run next and whether it did. Lampwick learns the schedule from the runs your app reports, so a task appears after its first run without any setup. ## Requirements - The [Laravel package](/docs/laravel/) with the ingest daemon running (`php artisan lampwick:agent`). - `LAMPWICK_SCHEDULED_TASK_SAMPLE_RATE` at `1` (the default): missed-run alerts need every run. - At most 500 tasks per project. ## Check that it works 1. Open the project and go to **Schedule health** in the sidebar (under **Executions**, after **Scheduled tasks**). 2. After the scheduler runs, each task shows its cron in words ("every 30 seconds" for sub-minute tasks), **Last run**, **Next run** and a **Status**. | Status | Meaning | |---|---| | **On time** | The last run came when expected. | | **Late** | Past its expected run, still within the grace period. | | **Missed** | Past the grace period. The team was alerted. | | **Failing** | The latest run failed. | | **Unknown schedule** | The expression never matches within 5 years, such as `0 0 31 2 *`. It never alerts. | | **Muted** | You muted its alerts. | The next run comes from the cron expression, the timezone (an IANA name or an offset such as `+02:00`) and `repeat_seconds`, with the Laravel scheduler's daylight saving behavior. A `skipped` run counts as a run. ## Alerts Every alert goes to the whole team by push notification and email, and to the team's alert channels ([alerts](/docs/alerts/)). | Alert | When | |---|---| | **Missed: TASK** | No run within the grace period after the expected time. One alert, then nothing until it runs again. | | **Running again: TASK** | It runs again after a missed-run alert. | | **Failed: TASK** | A task starts failing, at most once per hour per task, not on every failed run. Without an exception, the alert says none was recorded: check the exit code. | | **Slow: TASK** | A run takes longer than the task's duration alert. At most once per hour. | | **No data** or **Scheduler not running** | The project sends nothing at all, or no task ran while at least 2 missed. One alert for the project instead of one per task. It asks whether the app is in maintenance mode (`php artisan down`). | | **Rescheduled: TASK (ignore the earlier alert)** | A task that alerted now runs on a new schedule. | ### The grace period A run is missed when it hasn't come by the expected time plus a margin. The margin is at least 5 minutes, or a fifth of the interval (up to an hour) when that is longer, plus 1.5 times the task's longest run of the last week, plus the runs of the project's other tasks due at the same time, since `schedule:run` runs them one after another. For 5 minutes after Lampwick restarts, it reports no missed runs. ## Set a duration alert, mute or forget a task Next to each task on **Schedule health**: - The duration select: **No duration alert** (the default), or **Alert if longer than 5 min** up to 480 min. Not for tasks with `runInBackground`, which only report their start. - **Mute** stops alerts for the task; **Unmute** turns them back on. - **Forget** stops expecting the task until it runs again. A task removed from your schedule shows **Missed** and is forgotten after 7 days. A task that was clearly replaced (same name with another cron or timezone, or renamed) is forgotten at once, without an alert. In the API: `listSchedules`, `updateSchedule` (`muted`, `max_runtime_minutes` up to 1440; `0` turns the duration alert off) and `deleteSchedule` to forget. ## Troubleshooting | What you see | Cause and fix | |---|---| | "No scheduled tasks yet" | No task has run since the package was installed. Check that the ingest daemon runs and that `schedule:run` is in your server's crontab. | | A task shows **Missed** but did run | `LAMPWICK_SCHEDULED_TASK_SAMPLE_RATE` is below 1, so some runs aren't sent. Set it to `1`. | | **Scheduler not running** after a deploy | The app is in maintenance mode, or the cron entry for `schedule:run` is gone. | | A task you deleted still shows **Missed** | Click **Forget**, or wait 7 days. | | "This project has 500 scheduled tasks, the maximum" | A new task is only tracked when it can replace the task that has been missing longest. Forget tasks you no longer run. | --- Source: https://lampwick.dev/docs/status-pages/ # Status pages: publish uptime and incidents > A public page at app.lampwick.dev/status/SLUG with the status and daily uptime (up to 90 days) of monitors you pick, plus incident and maintenance notices. A status page shows your customers the status and daily uptime of the [uptime monitors](/docs/uptime/) you pick, and the incident and maintenance notices you post. It lives at `https://app.lampwick.dev/status/SLUG`, also as JSON. ## Requirements - Owner of the team. Members can view the team's status pages, not change them. - At least one uptime monitor in one of the team's projects. - At most 10 status pages per team. ## Create and publish a page 1. Go to **Status pages** in the sidebar. 2. Under **New status page**, enter a **Title** and a **Slug**. If you own more than one team, pick the **Team**. Click **Create status page**. The page starts as a **Draft**. 3. Under **Monitors on the page**, tick the monitors to show, from any of the team's projects. 4. Under **Shown publicly**, give each a public name (required; filled in with the monitor's name). Use the arrows to set the order. 5. Click **Save and publish**. **Preview** shows the page, drafts too, to team members only. **Unpublish** takes it offline at once (`404`). ## Check that it works Open `https://app.lampwick.dev/status/SLUG` in a private window, with `SLUG` the slug you chose. The page needs no login, cookies or JavaScript. ## Slugs - 3 to 40 characters: `a-z`, `0-9` and `-`, not at the start or end. Unique across Lampwick. - Reserved words such as `api`, `admin`, `status` and `lampwick` can't be used. - A freed slug (renamed, or deleted with its page or team) stays reserved for that team for 30 days, and the old slug of a renamed page redirects to the new one. ## What the public page shows The overall status at the top: | Status | When | |---|---| | Major outage | All monitors with a status are down. | | Partial outage | Some are down. | | Ongoing incident | An incident is open, also when everything is up. | | Maintenance in progress | A maintenance notice has status in progress. | | All systems operational | Otherwise. | | Status not available yet | No monitor has a status yet. | Below that, each monitor's status and a bar per day (UTC): green from 99.5% uptime, orange from 95%, red below, grey without checks. The bar covers 90 days, or fewer when your data retention is shorter; on a phone, 30. A paused monitor shows **Monitoring paused**, a new one **Starting soon**. The page never shows monitor URLs, error messages, status codes, response times, project or team names, members or ids. ## Post a notice 1. Open the status page and go to **Post a notice**. 2. Pick the **Type**: incident or maintenance. 3. Enter a **Title** and, optionally, a **Message** in plain text (no HTML or Markdown). 4. Pick a **Status**. Incidents: investigating, identified, monitoring, resolved. Maintenance: scheduled, in progress, completed. 5. For maintenance, set **Starts** and **Ends** (UTC, optional). They show as "Sun 9 Nov, 02:00–03:00 UTC". 6. Click **Post notice**. Open notices come first, incidents before maintenance. **Mark resolved** or **Mark completed** closes one; closed notices stay visible for 14 days. At most 200 notices per page. ## JSON for your own site ```bash curl https://app.lampwick.dev/status/SLUG.json ``` It answers with `Access-Control-Allow-Origin: *` and has `title`, `description`, `url`, `status` (`operational`, `partial_outage`, `major_outage`, `maintenance` or `unknown`), `status_text`, `history_days`, `generated_at`, `components` and `notices`. Each component has `name`, `status` (`operational`, `outage`, `paused`, `pending` or `unknown`), `uptime` in percent and `days` as `[{date, uptime}]`. Both are cached for 30 seconds, but an owner's change shows at once. Each IP address can make 300 requests per 5 minutes, then gets `429`. ## Add a badge to your site The badge is a small image for your site's footer. It shows a colored dot and the page's overall status, the whole badge lightly tinted in that status color, and links to your status page. 1. Open the status page and go to **Add a badge to your site**. 2. Pick a **Theme**, **Style** and **Language**. The preview and the snippets change with them. 3. Click **Copy** next to the HTML or Markdown snippet and paste it into your site. ```html Status of Acme ``` The image is `https://app.lampwick.dev/status/SLUG/badge.svg`. Options go in the query string; anything else is ignored, and so is a value not listed here. | Option | Values | Default | |---|---|---| | `theme` | `light`, `dark`, `auto`. `auto` follows your page's CSS `color-scheme` when it sets one, and otherwise the visitor's light or dark mode, so a light-only site would show a dark badge to visitors in dark mode. Use `auto` only if your site switches too. | `light` | | `style` | `full` shows the status, `compact` only "Status" (in the chosen language). | `full` | | `label` | `off` shows only the dot (**Dot only** in the settings). | on | | `lang` | `en`, `nl`, `de`, `fr`, `es`. | `en` | The status is the one at the top of the public page, in 5 states: All systems operational (green), Partial outage (orange), Major outage (red), Ongoing incident (orange: an incident is open and no monitor is down) and Maintenance (blue). Before the first check it shows "No data yet" with a grey dot. A draft, an unpublished page or a slug that doesn't exist all get the same grey "Status unavailable" badge with status `404`. The badge never shows your page title, monitor names or anything else you typed. After a slug change, the old badge URL redirects for 30 days, like the page. Browsers cache the badge for 60 seconds, so a change shows on your site within about a minute. Each IP address can load 300 badges per minute. For your own design, read `status` from the [JSON](#json-for-your-own-site) instead. ## API `listStatusPages`, `createStatusPage`, `getStatusPage`, `updateStatusPage` (also publish and unpublish), `deleteStatusPage`, and `createStatusNotice`, `updateStatusNotice`, `deleteStatusNotice`. On the [MCP server](/docs/mcp/), agents can only list and read status pages. ## Troubleshooting | What you see | Cause and fix | |---|---| | "a published status page needs at least one monitor" | Tick a monitor under **Monitors on the page** before you publish. | | "that slug is already taken; pick another" | Another page uses it, or used it in the last 30 days. Pick another slug. | | The public URL answers `404` | The page is a draft or was unpublished. Click **Save and publish**. | | A monitor disappeared from the page | The monitor or its project was deleted. | To tell your team when a monitor goes down, see [alerts](/docs/alerts/). --- Source: https://lampwick.dev/docs/funnels/ # Funnels, user paths and feature usage > Funnels, user paths and feature usage from the requests and server-sent custom events of signed-in users, with alerts when a step's conversion drops. Lampwick builds funnels, user paths and usage per route from the requests it already receives, so there is nothing to add to your app to start. A step without a request of its own, such as a step of a single-page app, you can send from your server as a **custom event** with the user id. A funnel step, a user path or a usage row can be a route, an event or a page. Lampwick counts **signed-in users** only, because a request or event needs a user id to be followed to the next one. Custom events and page views from the browser script are anonymous counts: they show in their own lists, never in funnels, paths or retention. ## Requirements - Requests with a user. The [Laravel package](/docs/laravel/) sends Laravel's authenticated user; with [OpenTelemetry](/docs/opentelemetry/), set `enduser.id` on the request span. - Optional: custom events your server sends with a user (see [Custom events and page views](#custom-events-and-page-views)). - Funnels per project: Free 1, Pro 3, Team 20, Business unlimited. - Funnel alerts: the Team plan and up. Numbers update hourly. A new funnel is filled from the data Lampwick keeps (your plan's retention) within a few minutes. ## Create a funnel 1. Open the project and go to **Funnels** in the sidebar. 2. Click **New funnel**, give it a name and pick 2 to 8 steps in order, for example `GET /cart`, the event `checkout.started`, `POST /checkout`. Routes, custom events and page views are listed in their own groups, by use in the last 7 days. 3. Pick the **Conversion window**: how long a user has from the first step to the last (7 days by default). 4. Click **Create funnel**. The funnel's page says "Lampwick is counting this funnel" while it fills the funnel from the kept requests. After a few minutes the **Steps** card shows the users per step. If it stays empty, see [Troubleshooting](#troubleshooting). A user enters the funnel with a request (or event, or page view) of the first step and moves on with one of the next step, in that order, within the window. Anything else in between doesn't matter. A request that answers with a 5xx doesn't count as reaching a step. After a user completes the funnel, or the window runs out, their next request to the first step starts a new attempt. From the API or an agent, pass the steps as text. A route is its label, an event `event:NAME`, a page `page:/path`: ```bash curl -X POST https://app.lampwick.dev/api/v1/projects/42/funnels \ -H "Authorization: Bearer $LAMPWICK_API_TOKEN" -H "Content-Type: application/json" \ -d '{"name": "Checkout", "steps": "GET /cart → event:checkout.started → POST /checkout → page:/orders/{id}"}' ``` An event or page doesn't have to have been sent yet: you can set up the funnel first. `GET /api/v1/projects/42/analytics/steps` lists the routes, events and pages the project has, each with the text to use. ## Read a funnel The funnel's page shows, for the users who entered in the range: | Section | What it shows | |---|---| | **Steps** | Users who reached each step, the share of the previous step and of the start, how many dropped off, the median time from the previous step, and 5xx on the step's route. | | **Why users drop off** | The step that loses the most users: how many reached it and how many went on, how users who hit a server error, an exception or a slow response on it went on against the others, the top issues on its routes, their server errors and slowest requests, and the conversion of that step per deploy. Each links to the issue, the route, the request or the deploy. | | **Problems on each step** | How users who hit a server error, an exception or a slow response (slower than the route's p95 over the 7 days before) on a step went on, against everyone else at that step. | | **Conversion by deploy** | Users grouped by the deploy that was live when they entered. Needs [deploys](/docs/deploys/). | **Why users drop off** reads the issues, server errors and slowest requests from all requests to the two routes of that step, over the range but at most the last 7 days. It is what `findDropOffs` gives an agent (see [For agents](#for-agents-mcp)). **Export CSV**, at the top right, downloads the steps as a CSV file: per step its route, users, conversion from the previous step and from the start (in %), users dropped, the median seconds to the next step, and the route's requests and server errors in the range. In every export, shares are percentages from 0 to 100 with one decimal (columns ending in `_pct`), durations are rounded (`_ms`, or whole seconds in `_secs`) and times are UTC. Names, usernames and routes come from your app. A value that starts with `=`, `+`, `-` or `@`, or has one of them right after a comma, semicolon or tab, gets a `'` in front, so a spreadsheet shows it as text instead of running it as a formula. "Can still complete" counts users who entered less than the conversion window ago and haven't reached the last step yet. Their numbers can still grow, so the conversion of the last days, and of a deploy that went out recently, looks lower than it will end up. ## Custom events and page views A **custom event** is something a user did that you name, like `checkout.completed` or `invite.sent`, with up to 20 properties. A **page view** is a page of your app a user saw. Both have their own lists, **Events** and **Pages** in the tabs on top of **Funnels** (or ⌘K), with their counts, and work as steps everywhere a route does. For funnels, paths and retention, send events from your server, with the user id, as a record of type `event` to [HTTP ingest](/docs/http-ingest/#custom-events-and-page-views). In a single-page app, send the key steps from your server, such as `checkout.completed` when the order is saved: ```bash curl -X POST https://app.lampwick.dev/ingest \ -H "Authorization: Bearer $LAMPWICK_TOKEN" -H "Content-Type: application/json" \ -d '[{"t": "event", "name": "checkout.completed", "user": "42", "props": {"plan": "pro"}}]' ``` `$LAMPWICK_TOKEN` is the project's ingest token, from the project's **Settings** page. In Laravel, the same request with the `Http` client: ```php use Illuminate\Support\Facades\Http; Http::withToken(config('lampwick.token'))->post('https://app.lampwick.dev/ingest', [ ['t' => 'event', 'name' => 'checkout.completed', 'user' => (string) $order->user_id, 'props' => ['plan' => 'pro']], ]); ``` A page view from your server works the same way, as a record of type `page` with the path and the user. With [OpenTelemetry](/docs/opentelemetry/), a log record with an event name (the Events API, or the `event.name` attribute) becomes a custom event: its other attributes are its properties and `enduser.id` its user. Such log records no longer show up as log lines. The [browser script](/docs/browser/#custom-events-and-page-views) counts events and page views too (`lampwick.track('checkout.started')`, `data-pages`), as a small bonus: anonymous counts with no cookies and nothing stored in the browser, so they need [no consent banner for Lampwick's analytics](/legal/dpa/#analytics-and-your-users-devices). They never carry a user, so they appear in **Custom events** and **Page views** and their totals, not in funnels, paths, usage per user, retention or active users. | Rule | Value | |---|---| | Event name | 1 to 100 letters, digits, spaces and `. _ - : /`. Anything else is dropped. | | Properties | At most 20, named with letters, digits and `_ . -`, up to 2 KiB in total. Strings (cut to 200 characters), numbers and booleans; other values are left out. Query strings are removed from URLs in them. | | Page path | The path only: query string and fragment are never stored. Segments that look like ids (numbers, UUIDs, hashes, tokens, email addresses, random-looking strings) become `{id}`, so `/orders/8812` and `/orders/9133` are one page, `/orders/{id}`. Readable slugs are kept; for personal ones, send the route: `lampwick.page('/u/:name')`. | | Without a user | Counted under **Custom events** and **Page views**, never in funnels, paths, usage, retention or active users. Events and page views from the browser script are always without a user. | | Events quota | From your server or OpenTelemetry, custom events count toward your plan's events, like requests. From the browser script they have a monthly budget of their own, so a flood through the public key never blocks your ingest: 100k (Free), 1M (Pro), 5M (Team), 20M (Business), at most a fifteenth of it per day. Team owners get an email when it runs out. | To be notified when an event comes in, such as `checkout.completed`, add an [event alert](/docs/alerts/#event-alerts). Events and page views with a user (from your server) make them active, so they count in **Users** → **Active users** and retention too. Erasing a user removes their id and their events' properties; event names and page paths (and their labels in the lists) stay, as they hold nothing about the user. Don't put names, email addresses or other personal data, such as health details, in event names, properties or paths. ## Funnel alerts Click **Turn on alerts** on a funnel to tell the team when a step's conversion drops. Every hour Lampwick compares, per step, how many users went on to the next step within an hour, for users who reached the step in the last 6 hours, against the 7 days before. It alerts only when all of these hold: - at least 30 users reached the step recently and 100 in the 7 days before, - conversion dropped by at least 10 percentage points and by at least a quarter, - the drop is unlikely to be chance (a one-sided two-proportion z-test, z of 3 or more). The alert says which step dropped, since when and by how much, and names the deploy that went live in the day before, the 5xx and the top issue on the step's routes. For example: > **Conversion dropped: Checkout, step 5 (POST /checkout)** > Since 08:00 UTC, 41% of the 52 users who reached GET /cart went on to POST /checkout (against 72% in the 7 days before). Deploy v2026.11.10 went live shortly before. 15 × 5xx on these steps. Top issue: #5 App\Exceptions\PaymentFailedException. On a plan without funnel alerts (after a downgrade, say), the funnel keeps its setting but the checks pause; the funnel's page shows **Alerts paused**. It goes to the whole team by push notification, email and [alert channels](/docs/alerts/), at most once per step per day. On the iPhone app it follows the **Issues** notification switch. ## User paths **User paths** shows, for one route, event or page, where signed-in users came from and where they went next in the same session, where sessions started and ended, and the most common 3-step flows from and through it. Requests, custom events and page views all count, in the order they happened; events and pages have an **event** or **page** label. A session ends after 30 minutes without any of them. Open it from **Funnels** → **User paths**, from a step of a funnel, or from a route's, event's or page's own page. ## Feature usage **Requests** shows per route the distinct signed-in users per day and their share of the day's active users, with an arrow when the share changed against the period before. **Custom events** and **Page views** show the same per event and page. **Users** → **Retention** can filter on users who used a route, had an event or viewed a page. Requests has two more tabs: - **New in a release**: routes that first answered after Lampwick started receiving data, with the release they came in and their adoption per day since. - **Not hit in 7 days**: routes Lampwick has seen within your retention that had no request since. Routes that never had a request aren't known to Lampwick. Users per day are counted after the day ends (UTC), so today shows up tomorrow. **Export CSV**, at the top right, downloads the table of the tab you are on, with its sort and filter: for **All routes** the columns it shows (route, status classes, users per day, share of active users and its change in points, count, average, p95 and total time in milliseconds, last seen); for the other tabs the routes they list. The route's id (`grp`) comes last. ## For agents (MCP) `findDropOffs` answers "where do users drop off, and why" in one call: for a funnel, the step that loses the most users with its conversion, the impact of errors and slow responses, the top issues, server errors and slowest requests on it; for a start route such as `GET /cart`, the most common path from it and where sessions end. `createFunnel`, `getFunnel`, `getRoutePaths` and `listRouteUsage` (with `kind=all` for events and pages) are tools too, and `listAnalyticsSteps` lists what can be a step; `getFunnel` and `listRouteUsage` take `format=csv` for the CSV exports. See [MCP](/docs/mcp/). ## Troubleshooting | Message or symptom | Cause and fix | |---|---| | `step 2: no route "POST /chekout" in this project` | The route must have had a request within your retention. Copy the label from **Requests**. For an event or page, write `event:NAME` or `page:/path`. | | `step 2: "" is not an event name` | Event names are letters, digits, spaces and `. _ - : /`, up to 100 characters. | | An event doesn't show up | Check the name against the rules above. In a funnel, an event needs a user: send it from your server with the user id. Events from the browser script, and others without a user, only count. | | `a project can have 1 funnel on the Free plan` | Delete a funnel or upgrade the team's plan. | | `the Free plan doesn't include funnel alerts` | Alerts need the Team plan. | | The funnel shows no users | Only signed-in users count. Check that **Users** lists users for the project; the **Funnels** page also says so when the project has no requests with a user. | | Conversion looks low for the last day | Users who entered recently may still complete within the conversion window. Look at a range longer than the window. | --- Source: https://lampwick.dev/docs/alerts/ # Alerts: push, email, Slack, Discord, Teams > Lampwick alerts a project's team by push and email, and in Slack, Discord, Microsoft Teams or a signed webhook. Mute per monitor, task or account. Lampwick alerts every member of a project's team by push notification in the Lampwick iOS app and by email when a monitor, host check, site crawl or scheduled task needs attention. Alerts are on by default; there is nothing to set up. To send them to a chat channel or your own endpoint as well, add an [alert channel](#alert-channels). ## Requirements - Membership of the team that owns the project. - For email: a [confirmed address](#confirm-your-address). - For push: the Lampwick iOS app, signed in to your account. ## What alerts you | Alert | When | Docs | |---|---|---| | **Down: MONITOR** | An uptime monitor fails 2 checks in a row. | [Uptime](/docs/uptime/) | | **Up again: MONITOR** | The monitor is up again, only after a down alert. | [Uptime](/docs/uptime/) | | SSL certificate expires or is invalid | 14, 7, 3 and 1 days before expiry, and when it becomes invalid. | [SSL, domain & DNS](/docs/host-checks/) | | Domain expires | 30, 14, 7 and 1 days before the registration expires. | [SSL, domain & DNS](/docs/host-checks/) | | **DNS records changed** | NS or MX records changed, confirmed by a second lookup. | [SSL, domain & DNS](/docs/host-checks/) | | **Broken links** | A site crawl found new broken links or mixed content. | [Site crawl](/docs/crawler/) | | **Missed**, **Failed**, **Slow**, **Running again** | A scheduled task missed a run, started failing or ran too long, and when it runs again. | [Schedule health](/docs/cron-health/) | | **Conversion dropped** | A funnel step converts clearly worse than in the 7 days before. Team plan and up, once you turn on alerts for the funnel. | [Funnel alerts](/docs/funnels/#funnel-alerts) | | Your rule's name or event | A custom event you made an event alert for came in. | [Event alerts](#event-alerts) | New and returning [issues](/docs/issues/) go to the iOS app and to [alert channels](#alert-channels), not by email. A down alert puts the reason first and shows only the host and path of the URL, never the query string, because alerts pass through Apple, the mail provider and lock screens: > **Down: Checkout API** > HTTP 404 Not Found · 2 failed checks in a row · api.example.com/health ## Alert channels {#alert-channels} An alert channel sends a team's alerts, and its new and reopened issues, to Slack, Discord, Microsoft Teams or your own webhook, next to push and email. Team owners add them; members see them. 1. Click **Alert channels** in the sidebar. 2. Under **Add a channel**, pick the **Kind**, give it a **Name** (such as `#ops`) and paste the **Webhook URL**. 3. Under **Alerts of**, pick **All projects** of a team or one project, and click **Add channel**. 4. On the channel's page, click **Send test message**. | Kind | Webhook URL | |---|---| | Slack | `https://hooks.slack.com/…`, from Slack: **Apps → Incoming Webhooks** | | Discord | `https://discord.com/api/webhooks/…`, from the channel settings: **Integrations → Webhooks** | | Microsoft Teams | An `https://` workflow URL, from Teams: **Workflows** → "Post to a channel when a webhook request is received" | | Webhook | Your own `https://` endpoint (port 443 or 8443) | - The URL is stored encrypted and never shown again, only its host. Up to 20 channels per team. - Messages carry what alert emails carry: what happened, the reason and a link to the dashboard, with no URL query strings. **Include exception messages in issue notifications** on the channel's page adds the exception message; off, a message names only the exception class. - A channel gets at most 30 chat messages or 120 webhook requests an hour, and a team's channels together 300; the rest are skipped and logged, while push and email still go out. - A network error, timeout, `408`, `429` or `5xx` is retried 4 times, over about 8 minutes. Any other answer outside `2xx` fails at once. - **Deliveries** on the channel's page lists the latest messages with their status and response code. - In the API: `listAlertChannels`, `createAlertChannel`, `updateAlertChannel`, `testAlertChannel` and `deleteAlertChannel`. They are not MCP tools: where alerts go is decided by people. ### Webhook requests {#webhook-requests} A webhook channel gets a `POST` with a JSON body and these headers: | Header | Value | |---|---| | `X-Lampwick-Event` | The kind, such as `monitor_down`, `new_issue`, `regression` or `test` | | `X-Lampwick-Delivery` | The delivery id, the same on retries | | `X-Lampwick-Timestamp` | Unix seconds of this attempt | | `X-Lampwick-Signature` | `v1=` and the hex HMAC-SHA256 of `TIMESTAMP.BODY` with the channel's signing secret | The signing secret (`whsec_…`) is on the channel's page. Compare the signature in constant time and refuse old timestamps. ```json { "id": 1042, "event": "monitor_down", "title": "Down: Checkout API", "body": "HTTP 404 Not Found · 2 failed checks in a row · api.example.com/health", "url": "https://app.lampwick.dev/…", "created_at": "2026-11-09T10:15:00+00:00", "project": { "id": 7, "name": "Acme Shop" }, "team": { "id": 3, "name": "Acme" }, "alert_id": 881, "subject_id": 12, "issue_id": null, "issue_ref": null } ``` ## Event alerts {#event-alerts} An event alert tells you when something important happens in your app, such as a signup or a checkout: a [custom event](/docs/funnels/#custom-events-and-page-views) with a name you pick comes in. It works for events from your server, OpenTelemetry and the [browser script](/docs/browser/#custom-events-and-page-views). 1. Open **Events** (the tab next to **Funnels**) → **Event alerts**, or open an event and click **Notify me when this happens**. 2. Pick the event and, optionally, up to 3 conditions on its properties, such as `plan` = `pro`. All of them must hold. 3. Pick when: **Every time** (up to 10 an hour), **Hourly digest** or **Daily digest** (days in UTC). 4. Pick where: push to the team's iPhones, email, and the team's [alert channels](#alert-channels). 5. Click **Create alert**, then **Send test** to see what it looks like. The notification's title is the rule's name, or the event name. The body shows only the properties you list under **Show in the notification**, or the project's name when you list none; nothing else of the event, and never the user. Digests say how many: > **New Pro checkout** > amount: 49 · plan: pro > **user.signed_up** > 12× user.signed_up in the last hour Tapping it opens the event's list in Lampwick. Once an event has an alert, its page shows **Alerts (1)** instead of **Notify me when this happens**. | Rule | Value | |---|---| | Every time | At most 10 notifications per rule per hour. The rest of that hour comes in one summary when the hour is over: `25 more checkout.completed in the last hour (35 in total)`. | | Digests | One notification at the end of the hour, or of the UTC day (`… yesterday (UTC)`), only when the event came in. | | Delay | Usually within seconds for **Every time**; a digest about 2 minutes after its hour or day ends. | | Browser events | Anyone with your public browser key can send any event with any properties. A rule without conditions counts browser events; a rule with conditions counts only your server's events, unless you tick **Count browser events too**. Their property values are never shown. Browser events are anonymous. | | Push | Follows the **Issues** switch in the app's notification settings, and project mutes. Event alerts have their own budget of 30 pushes per project per hour, so they never hold back a monitor's down alert. | | Alert channels | Event alerts have hourly channel budgets of their own, the same size as the other alerts', so they never hold back a monitor's down alert either. | | Email | Goes to members with alert emails on and a [confirmed address](#confirm-your-address), batched like other alerts. | | Send test | Titled **Test: …**, with `…` for the shown properties. One a minute per project; tests don't use up the push and channel budgets. | | Rules per project | Free 1, Pro 5, Team 25, Business unlimited, and at most 100 on any plan. After a downgrade, rules beyond the limit pause (oldest kept) and send nothing, not even a test. | | Kept | The shown values of events still to be reported are kept until their notification is sent, and 2 days after. Sent notifications are in the alert history for 30 days. Erasing an app user clears the values still kept for the project's event alerts; notifications already sent keep their text. | In the API and the [MCP server](/docs/mcp/): `listEventAlerts`, `createEventAlert`, `getEventAlert`, `updateEventAlert`, `deleteEventAlert` and `testEventAlert`. For example, `{"event": "checkout.completed", "filter": {"plan": "pro"}, "show": ["amount"], "mode": "every", "push": true, "channels": true}`; add `"browser": true` to count browser events with a filter too. There is no endpoint to send a notification directly: event alerts react to the events your app already sends. ## Check that it works On a monitor's page, the **Alerts** card says who gets what and shows your own alert email setting. On **Account & security**, the **Alert emails** card shows **On** when emails will reach you. ## Fewer alerts on purpose | Rule | What it does | |---|---| | Flapping | A monitor sends at most one down alert per 30 minutes. If it goes down again within that time, the alert comes after the 30 minutes, if it is still down. A new URL starts over. | | Quiet starts | A new monitor that comes up alerts nothing. | | Batching | Emails wait 1 minute, so alerts close together arrive in one mail (up to 50 per mail). | | Per address | At most 20 alert emails per address per hour; the rest go with the next mail. | | Per project | Above 30 alerts in a project in an hour, further alerts go by email only. | ## Mute alerts | What | Where | API | |---|---|---| | One monitor, for the whole team | Monitor's page → **Mute alerts for everyone**, or ⌘ K. **Unmute alerts** turns them back on. | `updateMonitor` with `notify: false` | | One scheduled task | **Schedule health** → **Mute** next to the task | `updateSchedule` with `muted: true` | | All alert emails, for you only | **Account & security** → **Turn off alert emails** | `updateMe` with `alert_emails: false` | A muted monitor keeps running its checks, host checks and crawl, and shows a **Muted** badge in the monitor list. ## Confirm your address {#confirm-your-address} Alert emails go only to addresses confirmed by mail: the signup verification link, a password reset link or a mailed team invite. 1. Go to **Account & security**. 2. If the **Alert emails** card says **Not confirmed**, click **Send confirmation link**. 3. Open the link in the email. ## Troubleshooting | What you see | Cause and fix | |---|---| | No alert emails | On **Account & security**, check the **Alert emails** card: **Off** means you turned them off, **Not confirmed** means you need to confirm your address. | | No alert for a monitor that is down | The monitor is muted (**Muted** badge), or it alerted less than 30 minutes ago. | | Alerts on your phone stopped during a big outage | Over 30 alerts in a project in an hour go by email only. | | No event alert | The rule is **Off** or **Paused** (beyond your plan's rules per project), the event name differs (it must match exactly), a condition doesn't hold, or the hourly limit of **Every time** was reached: the rest comes in the summary after the hour. | | A colleague gets no alerts | They aren't a member of the project's team. [Invite them](/docs/teams/). | | A channel shows **Failed** deliveries | The webhook URL was revoked in Slack, Discord or Teams, or your endpoint answered with an error. The response code is in **Deliveries**; give the channel a new URL on its page. | | A channel shows **Skipped** deliveries | It hit its hourly limit during a burst of alerts. Push and email still went out. | --- Source: https://lampwick.dev/docs/api/ # Use the Lampwick JSON API and OpenAPI spec > Everything the dashboard shows or does is in the JSON API under https://app.lampwick.dev/api/v1, described by an OpenAPI 3.1 document. The JSON API under `https://app.lampwick.dev/api/v1` does everything the dashboard does, for scripts, a CLI or your own integrations. The dashboard's ⌘ K palette runs on the same API. ## Requirements - A personal API token, created under **API tokens** on **Account & security**. It sees what your account sees: your teams and their projects. ## Make your first request ```bash export LAMPWICK_TOKEN=YOUR_LAMPWICK_TOKEN curl -H "Authorization: Bearer $LAMPWICK_TOKEN" https://app.lampwick.dev/api/v1/projects ``` `YOUR_LAMPWICK_TOKEN` is your personal token, which starts with `lwp_`. ## Check that it works The answer is a JSON object with your projects, each with its `team`. A `401` means the token is wrong, expired or revoked. ## The OpenAPI document - `GET https://app.lampwick.dev/api/v1/openapi.json` (no token needed) is an OpenAPI 3.1 document of the whole API, to generate a client or CLI from. - Every operation has an `operationId` (`listIssues`, `getIssue`, `updateIssue`, …). Response schemas are complete: all keys are required, and what can be empty is nullable. - `GET https://app.lampwick.dev/api/v1` lists all endpoints and conventions in compact form. ## Tokens - Lampwick stores only a hash, so you see a token once. Up to 50 tokens per account, with an optional expiry. - A token gives the same access as your signed-in session, unless you limit it: see below. - Resetting your password or turning on [two-factor authentication](/docs/teams/#turn-on-two-factor-authentication) revokes all your tokens. ### Limit a token to some teams or projects When you create a token, **Teams and projects** is **All teams and projects** by default: every team you are in, also teams you join later. Choose **Only these** to tick whole teams (their projects added later are included) or, under **Only some projects**, single projects. - Outside its teams and projects, a limited token gets `404`, as if the project or team doesn't exist. Lists and search only show what is inside. - A token limited to single projects can't change team settings, members or billing, and can't create projects. - Account-wide calls (tokens, connected clients, account settings, new teams, status pages, alert channels) give `403 forbidden` with a limited token. - If every team and project of a limit is deleted, the token reaches nothing. It never falls back to everything. - With the API: `POST /api/v1/tokens` with `"limited_to": {"teams": [12], "projects": [345]}`. `listTokens` and `getMe` show `limited_to` (`null` means all teams). ## Conventions | Topic | Rule | |---|---| | Times | RFC 3339, in UTC. | | Durations | `duration`, `avg`, `p95` and the like are in microseconds (1 ms = 1000). | | Ids | Everything has a public id such as `prj_3kTz9QaBc81x` (`public_id`). Paths take it, or the readable form: `acme/shop` for a project (`acme%2Fshop` in a URL), the URL name for a team, `#42` (`%2342`) for an issue's `ref` or a monitor's, deploy's, funnel's or scheduled task's `number`. The numeric `id` still works but is deprecated, and a bare number is always that `id`, never a `#` number: `/issues/42` is not issue `#42`. | | `range` | `1h`, `24h` (the default), `7d` or `14d`. | | `q` and `limit` | Search term (case-insensitive substring) and number of results, with a maximum per endpoint. | | Unknown parameters | Unknown query parameters or body fields, and invalid values, give `400`. | | Bodies | JSON, with `Content-Type: application/json`. | ## Errors Errors look like `{"error": {"code": "…", "message": "…"}}`. | Status | Meaning | |---|---| | `400 bad_request` | A parameter or body field is unknown or invalid. | | `401 unauthorized` | No token, or a wrong one. | | `403 forbidden` | Not allowed, for example an action for team owners done by a member, or an account-wide call with a token limited to some teams. | | `404 not_found` | It doesn't exist, or it belongs to a team you are not in. Lampwick doesn't tell the two apart. | | `409 conflict` | The change breaks a rule, such as removing the last owner of a team. | | `429 rate_limited` | Too many requests of one kind. | ## Examples ```bash api() { curl -fsS -H "Authorization: Bearer $LAMPWICK_TOKEN" -H 'content-type: application/json' "https://app.lampwick.dev/api/v1$@"; } api /projects | jq '.projects[] | {public_id, path, name, open_issues}' api '/projects/acme%2Fshop/issues?status=open&range=7d' | jq '.issues[] | {public_id, ref, class, message, recent}' api /projects/acme%2Fshop/issues/%2342 -X PATCH -d '{"status":"resolved"}' api '/projects/acme%2Fshop/groups/requests?sort=p95&limit=10' | jq '.groups[] | {label, p95_ms: (.p95/1000)}' api '/projects/acme%2Fshop/logs?level=error&q=timeout' ``` Replace `acme%2Fshop` with your project's `path` (`/acme/shop`, with `%2F` for the inner slash) or its `public_id`, and `%2342` (`#42`) with an issue's `ref`. To give an AI agent the same access, [connect it to the MCP server](/docs/mcp/). --- Source: https://lampwick.dev/docs/cli/ # Use Lampwick from the command line > The Lampwick CLI (`lampwick`) runs every API operation as a command, logs in through your browser, records deploys from CI and follows logs live. `lampwick` is the command line for Lampwick: every operation of the [API](/docs/api/) is a command, plus a browser login, deploy markers from CI and live logs. It runs on macOS and Linux, on arm64 and x64. > The `lampwick` CLI isn't published yet, so the install commands below don't work yet. Until then, use the [API](/docs/api/) (every command is one API call) or the [deploy recipes](/docs/deploys/). ## Install ```bash brew install lmpwck/tap/lampwick ``` Or with npm, or the install script, which checks the download's SHA-256: ```bash npx lampwick --version curl -fsSL https://lampwick.dev/install.sh | sh ``` The script installs to `/usr/local/bin` when it can write there, else to `~/.local/bin`. ## Log in ```bash lampwick login ``` Your browser opens Lampwick's consent page. Choose **Read & write** to change things from the command line, or **Read-only (recommended)** to only look, then click **Connect**. The login lasts 60 days from its last use and refreshes itself. It shows up under **Connected clients** on **Account & security** as *Lampwick CLI on YOUR-COMPUTER*. The login is stored in the macOS keychain, or in `~/.config/lampwick/credentials.json` (readable only by you) on Linux. Check it with: ```bash lampwick auth status ``` ```text server https://app.lampwick.dev account ada@example.com login browser login, read & write token from the macOS keychain ``` A browser login can do what a connected client can, and no more: it can't manage API tokens or connected clients, invite or change team members, change billing, alert channels or status pages, delete a team, rotate an ingest token or change a browser key. Use the dashboard, or log in with an API token: `lampwick login --with-token < token.txt`. `lampwick logout` removes the login and disconnects it. ## Link a folder to a project ```bash cd ~/code/webshop lampwick use acme/webshop ``` This writes `.lampwick` in the root of the git repository. It holds no secrets, so you can commit it. When it names a server other than app.lampwick.dev or the one you last logged in to, the CLI doesn't send `LAMPWICK_TOKEN` or a token from `.env` there until you name that server with `--server` or `LAMPWICK_URL` (a changed `.lampwick` in a repository can't collect your tokens). Commands in that folder then use the project when you leave it out. `lampwick init` does the same and also offers to write the project's ingest token to `.env` as `LAMPWICK_TOKEN`; it asks first, and `--yes` skips the question. Projects are `team/project`, a project's URL name when only one project has it, or an id. Issues are their number: `42`. Monitors and deploys take their id, or `'#3'` for the number in the dashboard URL (in quotes, or bash reads `#` as the start of a comment). ## Examples ```bash lampwick status ``` ```text northwind/storefront https://app.lampwick.dev/northwind/storefront Issues 12 open issues REF STATUS CLASS MESSAGE OCCURRENCES LAST SEEN #1 open …oquent\ModelNotFoundException No query results for model [… 181 1h ago #2 open …ception\NotFoundHttpException The route old-shop/index.php… 304 1h ago #4 open …alidation\ValidationException The quantity field must be a… 47 11h ago #9 open Swift_TransportException Expected response code 250 b… 4 20h ago #7 open RedisException read error on connection to … 5 22h ago and 7 more: lampwick issues Monitors all 3 up Deploy v2026.11.10 22h ago by github-actions ``` ```bash lampwick issues # open issues of the linked project lampwick issues resolve 2 # also: ignore, reopen lampwick issues acme/blog --status all --limit 20 lampwick logs --follow --level warning # live; times in your local time zone lampwick monitors create https://example.com --name Homepage --interval-secs 300 lampwick teams members invite acme bob@example.com --role member lampwick open # the project's dashboard page ``` `lampwick --help` lists every command, and `lampwick COMMAND --help` shows its arguments and the API operation behind it. Tables fit the width of your terminal; `lampwick issues get 42` (or `issues view 42`: every `get` is also `view`) shows one issue in full, with durations in ms. Each API field is a flag (`interval_secs` is `--interval-secs`). For a body the flags can't express, pass JSON: `--data '{…}'`, `--data @body.json`, or `--data -` for stdin. `lampwick api /projects/3/issues -q status=all` calls any API path. ## Shell completion `lampwick completion SHELL` prints a completion script for `bash`, `zsh`, `fish`, `elvish` or `powershell`. For zsh: ```bash lampwick completion zsh > "${fpath[1]}/_lampwick" ``` Then start a new shell. For bash, write it to `~/.local/share/bash-completion/completions/lampwick`; for fish, to `~/.config/fish/completions/lampwick.fish`. ## Output in scripts In a terminal you get tables with color; `NO_COLOR` turns color off. When the output is piped, it is tab-separated without a header, ids first: ```bash lampwick issues list --status all | cut -f2,3 ``` ```text 1 open 2 resolved ``` `--json` prints the API's JSON response as it is. Tables and tab-separated output never show tokens or secrets; `--json` does, because that is the API's answer. | Exit code | Meaning | |---|---| | `0` | Done | | `1` | The API refused (its code and message are printed, for example `project not found (404 not_found)`), or the server couldn't be reached | | `2` | Usage: an unknown command, a missing or invalid argument, a name that matches more than one project | | `4` | Not logged in, or the token was refused | A command that deletes something asks first in a terminal. In a script it needs `--yes`. ## Record deploys from CI `lampwick deploy` records a [deploy marker](/docs/deploys/). In CI it uses the project's ingest token from `LAMPWICK_TOKEN`, the same secret the deploy recipes use, and fills in the version, commit, author and repository from GitHub Actions, GitLab CI, Bitbucket Pipelines, Laravel Forge or Vercel variables, or from git. ```yaml - name: Record deploy in Lampwick env: LAMPWICK_TOKEN: ${{ secrets.LAMPWICK_TOKEN }} run: npx --yes lampwick deploy --no-fail ``` The version is `--version`, `LAMPWICK_DEPLOY`, the tag of the commit, or the first 12 characters of the commit. The environment is `--environment`, `VERCEL_ENV`, `CI_ENVIRONMENT_NAME` or `production`. `--dry-run` prints what it would send. A network error or a `5xx` is retried twice. `--no-fail` turns a failure into a warning, so a missing marker doesn't fail your deploy. On a Forge or Ploi server, `lampwick deploy` reads `LAMPWICK_TOKEN` from the site's `.env`. Logged in with your account instead, it records the deploy on the linked project. ## Environment variables | Variable | What it does | |---|---| | `LAMPWICK_TOKEN` | An API token (`lwp_…`) to use instead of the login, or a project's ingest token for `lampwick deploy` | | `LAMPWICK_URL` | The server, default `https://app.lampwick.dev`. It must be `https://`; plain `http://` only works for localhost. | | `LAMPWICK_PROJECT` | The project to use when you leave it out, instead of `.lampwick` | | `LAMPWICK_CONFIG_DIR` | Where the login and settings are kept, default `~/.config/lampwick` | | `NO_COLOR` | No color in tables | ## Troubleshooting | Message | Cause | Fix | |---|---|---| | `not logged in to https://app.lampwick.dev: run lampwick login, or set LAMPWICK_TOKEN to an API token` | No login for this server. | Run `lampwick login`, or set `LAMPWICK_TOKEN` in CI. | | `this API token is read-only: it can't change anything (403 read_only_token)` | You chose **Read-only** when you logged in, or the token is read-only. | Run `lampwick login` again and choose **Read & write**. | | `"blog" is more than one project: acme/blog, side/blog; use team/project` | Two teams have a project with that URL name. | Use `team/project`. | | `which project? Give it (team/project), or link this folder` | No project given and the folder isn't linked. | Run `lampwick use team/project`. | | `your login expired or was revoked` | 60 days unused, a password reset, a change of two-factor settings, or **Disconnect** on the account page. | Run `lampwick login`. | Next: [record deploys](/docs/deploys/) on every release. --- Source: https://lampwick.dev/docs/mcp/ # Connect Lampwick to your AI agent > Lampwick's MCP server is at https://app.lampwick.dev/mcp. Add it as a connector and sign in, or give Claude Code, Codex or Cursor a personal token. The [MCP](https://modelcontextprotocol.io) server at `https://app.lampwick.dev/mcp` lets an AI agent read your issues, requests, logs and monitors and act on them. Connect a client by signing in, where it supports connectors (claude.ai, ChatGPT, Cursor, VS Code, Claude Code), or with a personal token. Every tool call is an [API](/docs/api/) request with the access you gave it: read-only shows the agent your teams' data without letting it change anything, read & write lets it do what you can, except delete a team, rotate an ingest token or change a browser key. No Lampwick account yet? Your agent can [create a workspace for you](#agent-workspaces) and hand you a link to claim it. ## Connect by signing in {#sign-in} Clients with connectors connect without a token: you log in to Lampwick and choose what the client may do. claude.ai, Claude Desktop, the Claude mobile apps, ChatGPT, Cursor, VS Code and Claude Code support this. 1. In your client, add a custom connector (some clients call it a remote MCP server) with the URL `https://app.lampwick.dev/mcp`. Leave any client ID and secret fields empty. 2. The client opens Lampwick in your browser. Log in, with your two-factor code if you turned it on. 3. On **Connect CLIENT to Lampwick?**, choose **Read-only (recommended)** or **Read & write**. Under **Teams and projects**, keep **All teams and projects** or choose **Only these** and tick the teams (or, under **Only some projects**, single projects) the agent may see. Then click **Connect**. In claude.ai, connect Lampwick from [Claude's connector directory](https://claude.ai/directory/lampwick) (or add it under **Customize** → **Connectors** → **Add** → **Add custom connector**); Claude Desktop and the mobile apps then have it too. Step by step: [Connect Lampwick to Claude](/integrations/claude-ai/). In Cursor, open [Add Lampwick to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=lampwick&config=eyJ1cmwiOiJodHRwczovL2FwcC5sYW1wd2ljay5kZXYvbWNwIn0=) on the computer where Cursor is installed. Cursor asks to install the server `lampwick` with the URL `https://app.lampwick.dev/mcp` and no token; confirm, then sign in when Cursor asks. The link only works with Cursor installed. Without it, add the same URL to `~/.cursor/mcp.json`: ```json { "mcpServers": { "lampwick": { "url": "https://app.lampwick.dev/mcp" } } } ``` In Claude Code, add the server without a header, then run `/mcp` and sign in from there: ```bash claude mcp add --transport http lampwick https://app.lampwick.dev/mcp ``` | Access | What the agent can do | |---|---| | **Read-only** | Look at issues, requests, logs and monitors. It can't change anything. | | **Read & write** | Also resolve issues, change monitors and projects, and delete things. Never: delete a team, rotate a project's ingest token or change its browser key. | What an agent reads includes error messages and logs from your apps, which could contain instructions meant for it. With read-only access, those can't change anything. - A connected client uses every team of your account, or only the teams and projects you chose. Outside those, a tool call gets `404`, as if the project doesn't exist, and account-wide tools aren't listed. Its tokens work on `/mcp` only, not on `/api/v1`. - **Account & security** → **Connected clients** shows each client's teams; to change them, connect the client again. - It stays connected while it is used at least once every 60 days. - Disconnect it on **Account & security** → **Connected clients** → **Disconnect**. Resetting your password or turning two-factor authentication on or off disconnects every client. ## Connect with a personal token {#token} For clients without connectors, scripts and the [Claude API](#claude-api), use a personal token in the `Authorization` header. - Create the token under **API tokens** on **Account & security**. It starts with `lwp_`. Pick **Read-only (recommended)** unless the agent must change things: read-only tokens list only the read tools. For an agent that works on one client or project, choose **Only these** under **Teams and projects** ([limited tokens](/docs/api/#limit-a-token-to-some-teams-or-projects)). - The client must support the Streamable HTTP transport and a custom `Authorization` header. The examples read the token from the `LAMPWICK_TOKEN` environment variable: ```bash export LAMPWICK_TOKEN=YOUR_LAMPWICK_TOKEN ``` `YOUR_LAMPWICK_TOKEN` is your `lwp_…` token. Where a client can't read environment variables, paste it in place of `YOUR_LAMPWICK_TOKEN`. ### Token setup per client Jump to: [Claude Code](#claude-code) · [Codex CLI](#codex-cli) · [Cursor](#cursor) · [VS Code](#vs-code) · [Gemini CLI](#gemini-cli) · [Zed](#zed) · [Cline](#cline) · [Claude API](#claude-api) · [OpenAI and xAI](#openai-responses-api-and-xai) · [Other clients](#other-clients) ### Claude Code ```bash claude mcp add --transport http lampwick https://app.lampwick.dev/mcp --header "Authorization: Bearer $LAMPWICK_TOKEN" ``` Or share it with your team in the project's `.mcp.json`, where Claude Code fills in `${LAMPWICK_TOKEN}` from each person's environment: ```json { "mcpServers": { "lampwick": { "type": "http", "url": "https://app.lampwick.dev/mcp", "headers": { "Authorization": "Bearer ${LAMPWICK_TOKEN}" } } } } ``` ### Codex CLI In `~/.codex/config.toml`: ```toml [mcp_servers.lampwick] url = "https://app.lampwick.dev/mcp" bearer_token_env_var = "LAMPWICK_TOKEN" ``` ### Cursor In `~/.cursor/mcp.json`: ```json { "mcpServers": { "lampwick": { "url": "https://app.lampwick.dev/mcp", "headers": { "Authorization": "Bearer ${env:LAMPWICK_TOKEN}" } } } } ``` ### VS Code In `.vscode/mcp.json`. VS Code asks for the token once and stores it: ```json { "inputs": [ { "type": "promptString", "id": "lampwick-token", "description": "lampwick API token", "password": true } ], "servers": { "lampwick": { "type": "http", "url": "https://app.lampwick.dev/mcp", "headers": { "Authorization": "Bearer ${input:lampwick-token}" } } } } ``` ### Gemini CLI ```bash gemini mcp add --transport http --header "Authorization: Bearer $LAMPWICK_TOKEN" lampwick https://app.lampwick.dev/mcp ``` ### Zed In your Zed settings: ```json { "context_servers": { "lampwick": { "url": "https://app.lampwick.dev/mcp", "headers": { "Authorization": "Bearer YOUR_LAMPWICK_TOKEN" } } } } ``` ### Cline In Cline's MCP settings: ```json { "mcpServers": { "lampwick": { "type": "streamableHttp", "url": "https://app.lampwick.dev/mcp", "headers": { "Authorization": "Bearer YOUR_LAMPWICK_TOKEN" } } } } ``` ### Claude API With the MCP connector of the Messages API, send the beta header `anthropic-beta: mcp-client-2026-09-15` and add to the request: ```json { "mcp_servers": [ { "type": "url", "url": "https://app.lampwick.dev/mcp", "name": "lampwick", "authorization_token": "YOUR_LAMPWICK_TOKEN" } ], "tools": [ { "type": "mcp_toolset", "mcp_server_name": "lampwick" } ] } ``` ### OpenAI Responses API and xAI Add Lampwick as a tool: ```json { "type": "mcp", "server_label": "lampwick", "server_url": "https://app.lampwick.dev/mcp", "authorization": "YOUR_LAMPWICK_TOKEN", "require_approval": "always" } ``` For xAI, leave out `require_approval`. ### Other clients In other clients with a settings screen for MCP servers, add the URL `https://app.lampwick.dev/mcp` with the header `Authorization: Bearer YOUR_LAMPWICK_TOKEN`. ## Check that it works Ask your agent "Which Lampwick projects can you see?". It calls `listProjects` and lists the projects of your teams. ## What you can ask - "Which open issues in Acme Shop were first seen in the last 24 hours?" - "What are the 10 slowest routes by p95 this week?" - "Which uptime monitors are down right now?" - "Which scheduled tasks are failing or missed their last run?" - "Show the error logs from the last hour that mention timeout, then resolve issue #42." - "Where do users drop off in the Checkout funnel, and which errors are on that step?" - "How many users did issue #42 hit, and how many have we not seen since?" - "Which routes are new in the last release, and how many users use them?" For a full walkthrough from a new error to a reviewed fix, see [debug Laravel production errors with Claude Code](/guides/debug-laravel-production-errors-with-claude-code/). ## What the agent can do - Each API operation is a tool named after its `operationId` (`listIssues`, `getIssue`, `updateIssue`, …), with its path, query and body fields as arguments. Validation, access and owner rules are those of the API; an error comes back with `isError: true`. - Read tools carry `readOnlyHint`. Every tool that changes something carries `destructiveHint`, so clients that honor the hints ask before running it: deletes, rotations, and updates that pause or mute alerts, overwrite or rename. Only tools that just add something (a project, a monitor, a deploy marker, a crawl now) don't. - `getSetupStatus`, `getWorkspace` and `confirmWorkspaceAccess` are listed only for an agent's [workspace token](#agent-workspaces). Called with your own token or connection, they return an error that names the tools to use instead. - Not tools: token management, logging out, push devices, team invites, members and roles, and creating, changing or deleting status pages and notices. - Tools name things by what they are, in the words you use: `project: acme/shop`, `team: acme`, `issue: #12`, `monitor: #3`. Your client's permission prompt shows those, not internal ids. Results carry public ids (`prj_…`, `mon_…`), which the tools take too. Numbers need their `#`: a bare `12` is refused, so the agent can't pick the wrong issue. - When the client connects, Lampwick tells the agent its conventions: durations in microseconds, the `range` values, how to name projects and issues, and where to start. ## Trial workspaces created by an agent {#agent-workspaces} A coding agent can set up Lampwick before you have an account. It creates a trial workspace with `POST /api/v1/workspaces` and gets a workspace token (`lws_…`), which works on this MCP server too. With it, `tools/list` shows only what a workspace may do: projects, monitors, deploy markers, reading issues, events and logs, plus `getSetupStatus`, `getWorkspace` and `confirmWorkspaceAccess`. You claim the workspace from the link the agent gives you, and choose read-only or read & write for the agent. Once it confirms the code you give it, it keeps that access for 30 days after the claim; then it connects by [signing in](#sign-in). The flow, the limits and the errors: [Set up with your agent](/docs/agents/). ## Protocol details | Topic | Value | |---|---| | Endpoint | `POST https://app.lampwick.dev/mcp` | | Transport | Streamable HTTP, stateless: no sessions, no server-initiated stream (`GET /mcp` gives `405`) | | Authentication | OAuth 2.1 sign-in (access tokens `lwa_…`), or `Authorization: Bearer lwp_…` with a personal token; a dashboard session doesn't count | | Protocol versions | `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` | | Browsers | Any origin may call it (CORS, without credentials). It only takes bearer tokens and never reads cookies, so a dashboard session doesn't count | | Batches | Up to 50 messages; larger batches get one JSON-RPC error `-32600` | ## Troubleshooting | What you see | Cause and fix | |---|---| | `401` or an authorization error | The `Authorization` header is missing, or the token is wrong, expired or revoked. Create a new token on **Account & security**, or connect again by signing in. | | `403` on a tool that changes something | The connection or token is read-only. Connect again and choose **Read & write**, or use a full-access token. | | `405` | The client tried `GET /mcp` for a stream. Lampwick only takes `POST`; pick the Streamable HTTP transport. | | A project is missing | A connection or token only sees the teams of its account. Ask an owner to invite you to the team. | | A tool returns `403` on a read-and-write connection | That action is for team owners. | --- Source: https://lampwick.dev/docs/teams/ # Manage teams, members and two-factor auth > Projects belong to a team. Owners invite people by email and manage ingest tokens, deletes and the team; members use the projects. Plus 2FA. Every project belongs to a team, and you see the projects of every team you are a member of. ## Requirements - You own the team you get at signup. To invite people you must be an owner with a confirmed email address. - At most 20 teams that you own, 50 projects per team and 50 pending invites per team. ## Roles | | Member | Owner | |---|---|---| | View the team's projects, create and rename projects | yes | yes | | Handle issues, uptime monitors, scheduled tasks | yes | yes | | See and rotate a project's ingest token | no | yes | | Delete projects | no | yes | | Create, publish and change [status pages](/docs/status-pages/) | no | yes | | Invite and remove people, change roles | no | yes | | Rename or delete the team | no | yes | The ingest token lets anyone write data into a project, so members see "Only team owners can see the ingest token" on **Settings**, and the API returns `ingest_token: null`. ⌘ K doesn't show owner actions to members. ## Invite someone 1. Go to **Teams** in the sidebar and open the team. 2. Under **Invite people**, enter the email address and pick **Member** or **Owner**. 3. Click **Invite**. Lampwick emails them a link. The link works once, for 7 days, and only for an account with the invited address. A new invite replaces the old one, and an invite stops working when its sender is no longer an owner. Each owner can send 30 invites per hour; an address gets at most 3 invite emails per 24 hours. ## Accept an invite Open the link from the email: - Logged in with the invited address: click **Join TEAM**. - Logged in as someone else: **Log out and continue**. - Not logged in: **Log in to join**, or under **New here? Choose a password**, set a password and click **Create account and join**. The link proves the address, so the new account is confirmed at once. ## Check that it works The team's page lists the new person under **Members** with their role and join date. Until they accept, they are under **Pending invite**, where an owner can **Revoke** the invite. ## Change roles, remove people, leave On the team's page, an owner changes roles with the select next to each member and removes people with **Remove**. To leave, click **Leave** next to your own name. The last owner can't leave, be removed or be made a member: make someone else an owner first, or delete the team. Anyone who saw an ingest token as an owner can keep writing data with it after you remove them. Rotate each project's token: **Settings → Rotate tokens or delete the project → Rotate token**, or ⌘ K → **Rotate ingest token**. ## Turn on two-factor authentication 1. Go to **Account & security**. 2. Under **Two-factor authentication**, click **Set up two-factor authentication**. 3. Scan the QR code with an authenticator app such as 1Password, Google Authenticator or Authy, enter the 6-digit code and click **Turn on**. 4. Save the 10 recovery codes. Each works once if you lose your authenticator. Turning 2FA on logs out your other sessions and revokes all your API tokens; create new ones afterwards. **New recovery codes** replaces all existing codes. ## Delete your account At the bottom of **Account & security**, click **Delete account** and enter your password, plus a 2FA or recovery code when 2FA is on. This is blocked while you are the only owner of a team with other members. Teams where you are the only member are deleted with their projects. ## Troubleshooting | What you see | Cause and fix | |---|---| | "This invite is invalid, already used or expired" | Invites work once and for 7 days. Ask the team's owner for a new one. | | A project is missing | It belongs to a team you aren't a member of. Ask its owner for an invite. | | No **Invite people** card | You are a member, not an owner, of this team. | | You can't leave the team | You are its only owner. Make someone else an owner first. | | Your API tokens stopped working | Turning on 2FA, or resetting your password, revokes all your tokens. Create new ones on **Account & security**. | To give scripts or agents access to the team's projects, [create an API token](/docs/api/). --- Source: https://lampwick.dev/compare/ # Lampwick compared with other monitoring tools > Lampwick side by side with Nightwatch, Sentry, Flare and Oh Dear, plus lists of Laravel monitoring tools and alternatives, checked on vendor pages. We make Lampwick. Each comparison says where the other tool is the better pick, links every competitor fact to the vendor's own page and shows the date we checked it. - [Lampwick vs Flare](/compare/lampwick-vs-flare/): Flare is stronger on error workflow, Core Web Vitals and JS framework SDKs. Lampwick adds uptime, SSL, domain, DNS, cron and status pages. Checked 8 Nov 2026. - [Lampwick vs Laravel Nightwatch](/compare/lampwick-vs-nightwatch/): Nightwatch gives more events and history per dollar; its pricing explained. Lampwick adds uptime, SSL, DNS, cron and status pages. Verified 10 Nov 2026. - [Lampwick vs Oh Dear](/compare/lampwick-vs-oh-dear/): Oh Dear has more site checks, locations and alert channels. Lampwick adds errors, requests, jobs and logs from inside your app. Checked 10 Nov 2026. - [Lampwick vs PostHog for product analytics](/compare/lampwick-vs-posthog/): PostHog is a product analytics suite with autocapture, replay and flags. Lampwick monitors your app and adds funnels from its requests. Verified 12 Nov 2026. - [Lampwick vs Sentry for Laravel apps](/compare/lampwick-vs-sentry/): Sentry covers more languages, longer history and session replay. Lampwick adds uptime, SSL, domain, DNS checks and status pages. Verified 10 Nov 2026. - [Best Laravel monitoring tools (2026)](/compare/laravel-monitoring-tools/): 13 Laravel monitoring tools compared: Nightwatch, Flare, Sentry, Pulse, Oh Dear, Inspector, Lampwick and more. Prices and limits verified on 10 Nov 2026. - [Laravel Nightwatch alternatives (2026)](/compare/nightwatch-alternatives/): What Nightwatch doesn't do, what a typical app costs elsewhere, and the closest alternatives: NightOwl, Laritor, Flare, Sentry, Vigilant, Lampwick. - [Sentry alternatives (2026), with picks for Laravel](/compare/sentry-alternatives/): 10 Sentry alternatives: Rollbar, GlitchTip, Bugsink, Honeybadger, Better Stack, plus Flare, Nightwatch and Lampwick for Laravel. Verified 10 Nov 2026. - [UptimeRobot alternatives (2026): 6 tools compared](/compare/uptimerobot-alternatives/): UptimeRobot's free plan allows commercial use. Alternatives: Oh Dear, Better Stack, Healthchecks.io, Uptime Kuma, Pingdom, Lampwick. Verified 10 Nov 2026. --- Source: https://lampwick.dev/compare/lampwick-vs-flare/ # Lampwick vs Flare > Flare is stronger on error workflow, Core Web Vitals and JS framework SDKs. Lampwick adds uptime, SSL, domain, DNS, cron and status pages. Checked 8 Nov 2026. We make Lampwick. Here's where each tool is the better pick. Flare, from Spatie in Belgium, tracks errors, performance and logs for Laravel, PHP and JavaScript. If error workflow and frontend performance matter most, pick Flare. If you want app monitoring plus uptime, certificate, domain and cron checks in one tool, pick Lampwick. Last reviewed 8 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes. ## Prices, limits and features side by side | | Lampwick | Flare | |---|---|---| | Price model | Per team per month, by events | Per month, with separate limits for errors, spans and logs[^fl-pricing] | | Plans | Free €0: 500k events / 3 days · Pro €29: 5M / 14 days · Team €79: 25M / 30 days · Business €199: 100M / 60 days | Hobby €9 · Standard €29 · Growth €69 · Premium €279 a month, a month free when billed yearly; 30 days on every plan[^fl-pricing] | | What's included | Events: every record (request, query, job, log, exception…) | Standard: 100k errors, 20M spans, 5M logs[^fl-pricing] | | Over the limit | No overage: an email at 80% and 100%, then new data is refused until the 1st or an upgrade | No overage: new data of that type is refused until the limit resets[^fl-pricing] | | Sample app (below) | €29 | €29, plus a separate uptime tool | | Company | EU | Spatie, Belgium[^fl-home] | | Data location | EU (Hetzner) | Not verified: not stated on the pages we read[^fl-dpa] | | Uptime, SSL, domain, DNS | Yes | Not listed[^fl-pricing] | | Cron health (missed runs) | Yes | Not listed[^fl-pricing] | | Broken links, status pages | Yes (Pro and up) | Not listed[^fl-pricing] | | Alerts | iOS push, email, Slack, Discord, Microsoft Teams, signed webhooks | Email, Slack, SMS, Discord, Microsoft Teams, webhooks[^fl-notify] | | API / MCP | REST API; MCP server with OAuth 2.1 sign-in (read-only or read & write) or a personal token | MCP server with OAuth 2.1 or a personal token[^fl-mcp] | | Frameworks | Laravel package; browser errors with a script tag; any app over HTTP | Laravel, PHP, JavaScript (React, Vue, Svelte)[^fl-home] | | Free tier | Yes: 1 project, 1 seat | No; 10-day trial[^fl-pricing] | | Self-hosting | No | Not listed[^fl-pricing] | ## What does a typical app cost? The sample: 2 Laravel apps, 3M events a month in total, 5 people, 10 uptime monitors with SSL checks. - Lampwick Pro, €29: 5M events, 5 projects, unlimited seats, 20 monitors every minute, 14 days of history. - Flare Standard, €29 a month or €319 a year billed yearly, with 30 days of history.[^fl-pricing] (Hobby allows 1 project and 1 user.) Flare counts errors, spans and logs, not events; 3M events likely fit in 20M spans, but we haven't verified how Flare counts them, so check your own volume. - The 10 monitors need a second tool: Better Stack's free tier (for personal projects)[^bs-pricing] or Oh Dear Freelance at $55.[^od-pricing] Prices exclude VAT; we don't convert currencies. Lampwick counts one event per record your app sends (a request, query, job, log entry or exception). Uptime checks aren't events (monitors have their own limit per plan), and SSL, domain and DNS checks, alerts and schedule health count toward no limit. ## Who wins on each topic ### Error tracking: Flare wins In Flare you resolve, snooze and assign errors, add notes, and see errors by user, URL, job and version.[^fl-mcp] It also catches frontend errors and Core Web Vitals from JavaScript, React, Vue and Svelte.[^fl-home] Lampwick groups exceptions into issues with the stack trace and the users hit, from your server and, with a [script tag](/docs/browser/), from your visitors' browsers. It has no framework SDKs, no source maps and no Core Web Vitals. ### Local error pages: Flare wins Spatie makes Ignition, Laravel's local error page, and `spatie/laravel-error-share`, which adds a **Share** button to it without a Flare subscription.[^fl-share] Lampwick has nothing for local development. ### Performance and logs: tie Both show slow routes, queries, outgoing HTTP, traces and searchable logs. Lampwick adds a timeline per request, job or command. ### Retention: Flare wins on the lower plans Flare keeps 30 days on every plan;[^fl-pricing] Lampwick keeps 14 on Pro, 30 on Team and 60 on Business. ### Uptime, SSL, domain, DNS and cron: Lampwick wins Lampwick checks URLs, warns before a certificate or domain expires, watches NS and MX records and alerts when a scheduled task is missed. Flare lists none of these.[^fl-pricing] ### Status pages and broken links: Lampwick wins From Pro, Lampwick has public status pages with incident and maintenance notices, and a daily crawl for broken links and mixed content. Flare lists neither.[^fl-pricing] ### Alerts: tie Flare sends to email, Slack, SMS, Discord, Microsoft Teams and webhooks.[^fl-notify] Lampwick sends to iOS push, email, Slack, Discord, Microsoft Teams and signed webhooks ([alert channels](/docs/alerts/#alert-channels)). Flare adds SMS; Lampwick adds push from its iOS app. ### MCP: tie Flare's MCP server uses OAuth 2.1 or a personal token, read-only by default; write scopes let an agent resolve, snooze or assign errors.[^fl-mcp] Lampwick's [MCP server](/docs/mcp/) works the same way, OAuth 2.1 with read-only or read & write, or a personal token, and also covers monitors, certificates and cron. ### Data location: Lampwick states it Both are EU companies. We couldn't find where Flare hosts data on its home, pricing or DPA pages.[^fl-dpa] Lampwick and the services that see your data (Hetzner, Lettermint) are in the EU. ## When to pick Flare instead - Errors are what you watch most, with assignment, snoozing and notes. - You need Core Web Vitals or SDKs for React, Vue or Svelte. - Your alerts must go out by SMS. - You want 30 days of history on the €29 plan. [Start free](https://app.lampwick.dev/signup), or read how to [connect a Laravel app](/docs/laravel/). Also see [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/) and the [Nightwatch alternatives](/compare/nightwatch-alternatives/). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^fl-pricing]: [Flare pricing](https://flareapp.io/pricing), checked 8 Nov 2026. [^fl-home]: [Flare home page](https://flareapp.io/) ("Made in Belgium by Spatie"), checked 8 Nov 2026. [^fl-dpa]: [Flare DPA page](https://flareapp.io/dpa) (controller "Flare (Facade BV)"; no hosting location or subprocessor list on the page), checked 8 Nov 2026. [^fl-notify]: [Flare notification docs](https://flareapp.io/docs/notifications/configuring-notifications), checked 8 Nov 2026. [^fl-mcp]: [Flare MCP server docs](https://flareapp.io/docs/flare/general/our-mcp-server.md?nav=false), checked 8 Nov 2026. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), checked 8 Nov 2026. [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), checked 8 Nov 2026. [^fl-share]: [Ignition](https://flareapp.io/ignition) and [Introducing Laravel Error Share](https://flareapp.io/blog/introducing-laravel-error-share), checked 8 Nov 2026. --- Source: https://lampwick.dev/compare/lampwick-vs-nightwatch/ # Lampwick vs Laravel Nightwatch > Nightwatch gives more events and history per dollar; its pricing explained. Lampwick adds uptime, SSL, DNS, cron and status pages. Verified 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. For Laravel application monitoring alone, Nightwatch is cheaper and keeps data longer. Pick Lampwick if you also want uptime, certificate, domain, DNS and cron checks, status pages and a link crawler on the same bill, from an EU company. Last reviewed 10 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes with the date we verified them. ## Lampwick's Laravel package uses Nightwatch's instrumentation Lampwick's Laravel package requires `laravel/nightwatch` (`^1.24`, MIT-licensed) and uses its instrumentation inside your app.[^nw-mit] Our own ingest daemon (`php artisan lampwick:agent`) then sends the records to Lampwick instead ([Laravel docs](/docs/laravel/)). So both capture the same data; what differs is everything after that. When Laravel changes its package, we follow. ## Prices, limits and features side by side | | Lampwick | Nightwatch | |---|---|---| | Price model | Per team per month, by events | Per organization per month, by events[^nw-pricing] | | Plans | Free; from €29: 5M events / 14 days ([all plans](/pricing/)) | Free; from $20: 7.5M events / 30 days ([table below](#nightwatch-pricing-explained))[^nw-pricing] | | Extra events | Not published | $0.20 to $0.50 per 100k depending on plan, with a spend cap[^nw-pricing] | | Sample app (below) | €29 | $20, plus a separate uptime tool | | Company | NIO Media, Enschede, Netherlands (EU) | Laravel Holdings Inc., New York, US[^nw-terms] | | Data location | EU (Hetzner) | US or EU data centers[^nw-pricing] | | Uptime checks | Yes, every 30 s to 1 hour | Not in the docs[^nw-docs] | | SSL, domain, DNS checks | Yes | Not in the docs[^nw-docs] | | Cron health (missed runs) | Yes | Records runs and next run time; no missed-run alert in the docs[^nw-sched] | | Broken links and mixed content | Yes, daily crawl (Pro and up) | Not in the docs[^nw-docs] | | Status pages | Yes, up to 10 per team (Pro and up) | Not in the docs[^nw-docs] | | Alerts | iOS push, email, Slack, Discord, Microsoft Teams, signed webhooks | Slack and webhooks; Linear for issues[^nw-docs] | | API / MCP | REST API; MCP server with OAuth sign-in (read-only or read & write) or a personal token | MCP server with OAuth: applications and issues[^nw-mcp] | | Frameworks | Laravel package; any app over HTTP | Laravel only[^nw-pricing] | | Seats, apps | Free: 1 seat, 1 project. Paid: unlimited seats, 5 to unlimited projects | Unlimited seats, applications and environments on every plan[^nw-pricing] | | Self-hosting | No | No[^nw-pricing] | ## Nightwatch pricing explained Nightwatch charges per organization per month, by events. An event is one request, outgoing request, notification, job, query, mail, command, cache operation or scheduled task, and the plan table adds exceptions and logs.[^nw-pricing] Every plan has unlimited applications, environments and seats, so you pay only for volume and history.[^nw-pricing] | Plan | Price a month | Events included | Extra events | Lookback | Performance monitors | |---|---|---|---|---|---| | Free | $0 | 300k | $0.50 per 100k | 14 days | 1 | | Pro | $20 | 7.5M | $0.35 per 100k | 30 days | 10 | | Team | $60 | 30M | $0.35 per 100k | 60 days | 20 | | Business | $300 | 180M | $0.20 per 100k | 90 days | 30 | | Enterprise | Contact sales | Custom | Custom | Longer on request | Custom | Source: [Nightwatch pricing](https://laravel.com/nightwatch/pricing), verified on 2026-11-10.[^nw-pricing] What the table doesn't show: - **Extra events are opt-in.** Without an additional-events limit, Nightwatch pauses ingestion for the rest of the billing period once you pass your included events; your data stays. With a limit, it bills extra events in 100k blocks at the end of the cycle and pauses at the limit.[^nw-extra] - **Free projects pause** after 30 days without app activity, and one person can have at most 3 free organizations.[^nw-pricing] - **Upgrades apply at once**, with a prorated credit; downgrades wait until the end of the billing period.[^nw-subs] - **Sample rates and spending caps** keep a busy app on a smaller plan: Nightwatch lets you set both, and its docs have a page on filtering events.[^nw-pricing] [^nw-docs] Two worked examples from the table: an app sending 10M events a month on Pro pays $20 plus 25 blocks of 100k at $0.35, so $28.75, if you've set a limit of at least 2.5M extra events. The same app on Team pays $60 with no extras and gets 60 days instead of 30. Compared with Lampwick, Nightwatch includes more events and a longer lookback at every price point: $20 buys 7.5M events and 30 days, where Lampwick Pro's €29 buys 5M events and 14 days. Lampwick's plans include uptime monitors, status pages and site checks that Nightwatch doesn't offer. ## What does a typical app cost? The sample: 2 Laravel apps, 3M events a month in total, 5 people, 10 uptime monitors with SSL checks. - Lampwick Pro, €29: 5M events, 5 projects, unlimited seats, 20 monitors every minute, 14 days of history. - Nightwatch Pro, $20: 7.5M events, unlimited apps and seats, 30 days of history.[^nw-pricing] The 10 monitors need a second tool: Better Stack's free tier (for personal projects)[^bs-pricing] or Oh Dear Freelance at $55.[^od-pricing] Total: $20 to $75. Prices exclude VAT; we don't convert currencies. Lampwick counts one event per record your app sends (a request, query, job, log entry, exception or cache event), the same unit as Nightwatch. Uptime checks aren't events (monitors have their own limit per plan), and SSL, domain and DNS checks, alerts and schedule health count toward no limit. ## Who wins on each topic ### Events and retention per dollar: Nightwatch wins At every tier Nightwatch includes more events and keeps them longer. ### Application monitoring: tie, Nightwatch ahead on collaboration Both group exceptions into issues and show routes with p95 and a timeline per request or job. Nightwatch adds issue assignment and comments, per-route performance thresholds and a Linear integration.[^nw-home] Lampwick adds activity and errors per signed-in user, and logs tied to the request or job that wrote them. ### Uptime, SSL, domain and DNS: Lampwick wins Lampwick alerts 14, 7, 3 and 1 days before a certificate expires, looks up the domain daily over RDAP and checks NS and MX records hourly. Nightwatch's docs have no pages on any of this.[^nw-docs] ### Cron and scheduled tasks: Lampwick wins Lampwick works out each task's schedule from its runs and alerts when a task is missed, starts failing or runs too long, with one project alert when the whole scheduler stops. Nightwatch's scheduled-task page shows next run and execution times; we found no missed-run alert.[^nw-sched] ### Status pages and link crawling: Lampwick wins From Pro, Lampwick has public status pages with incident and maintenance notices, and a daily crawl for broken links and mixed content on up to 250 pages. Nightwatch's docs cover neither.[^nw-docs] ### Alerts: Lampwick wins Nightwatch sends issue updates to Slack and webhooks, and issues to Linear.[^nw-webhooks] Lampwick sends every alert and every new or reopened issue by iOS push, and alerts (not issues) by email, with no setup. Once a team owner adds an [alert channel](/docs/alerts/#alert-channels), both also go to Slack, Discord, Microsoft Teams or a signed webhook. Nightwatch still has Linear, which Lampwick doesn't. ### MCP: Lampwick wins Both MCP servers sign in with OAuth. Nightwatch's lets an agent browse issues and stack traces, change their status and comment.[^nw-mcp] Lampwick's [MCP server](/docs/mcp/) also covers requests, jobs, logs and monitors, and you choose read-only or read & write when you connect. Its agent can change an issue's status too, but Lampwick's issues have no comments. ### Data location: depends on what you need Both can keep your data in the EU.[^nw-pricing] The difference is the company: Laravel Holdings Inc. is based in New York, with terms under New York law.[^nw-terms] Lampwick is an EU company, and the services that see your data (Hetzner, Lettermint) are EU companies. ### Beyond Laravel: Lampwick, narrowly Nightwatch is for Laravel only.[^nw-pricing] Any app can send records to Lampwick's [HTTP ingest](/docs/http-ingest/), but without a package for other frameworks you build the records yourself. ## When to pick Nightwatch instead - You want the most events and the longest history for your money. - You want the first-party tool from the Laravel team. - Your issues must land in Linear. - You already have uptime and certificate checks you're happy with. Weighing more than these two? See the [Nightwatch alternatives](/compare/nightwatch-alternatives/). [Start free](https://app.lampwick.dev/signup), or read how to [connect a Laravel app](/docs/laravel/). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^nw-mit]: [laravel/nightwatch on GitHub](https://github.com/laravel/nightwatch): "open-sourced software licensed under the MIT license", verified on 2026-11-10. Our side: `packages/laravel/composer.json` and the package README. [^nw-pricing]: [Nightwatch pricing and FAQ](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. [^nw-terms]: [Nightwatch terms of service](https://laravel.com/legal/nightwatch/terms), verified on 2026-11-10. [^nw-docs]: [Nightwatch docs index](https://nightwatch.laravel.com/docs/llms.txt), verified on 2026-11-10: no pages on uptime, SSL, domain, DNS, status pages or link checks; alert integrations are Slack and webhooks, plus Linear. [^nw-sched]: [Nightwatch scheduled tasks docs](https://nightwatch.laravel.com/docs/scheduled-tasks.md), checked 8 Nov 2026. [^nw-mcp]: [Nightwatch MCP server docs](https://nightwatch.laravel.com/docs/mcp-server.md), checked 8 Nov 2026. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), verified on 2026-11-10. [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), verified on 2026-11-10. [^nw-extra]: [Nightwatch additional events docs](https://nightwatch.laravel.com/docs/additional-events.md), verified on 2026-11-10. [^nw-subs]: [Nightwatch subscriptions docs](https://nightwatch.laravel.com/docs/subscriptions.md), verified on 2026-11-10. [^nw-home]: [Nightwatch product page](https://laravel.com/nightwatch), checked 8 Nov 2026. [^nw-webhooks]: [Nightwatch webhooks docs](https://nightwatch.laravel.com/docs/webhooks.md) (issue opened, reopened, ignored, resolved; one webhook per application) and [Slack docs](https://nightwatch.laravel.com/docs/slack.md), checked 8 Nov 2026. --- Source: https://lampwick.dev/compare/lampwick-vs-oh-dear/ # Lampwick vs Oh Dear > Oh Dear has more site checks, locations and alert channels. Lampwick adds errors, requests, jobs and logs from inside your app. Checked 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. Oh Dear watches websites from outside. Lampwick does most of the same checks, but starts from inside your app: exceptions, requests, queries, jobs and logs. For site checks across many client sites, pick Oh Dear. For your own app's errors and uptime in one place, pick Lampwick. Last reviewed 10 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes and verified on 2026-11-10. ## Prices, limits and features side by side | | Lampwick | Oh Dear | |---|---|---| | Price model | Per team per month, by events; monitors included per plan | Per month, by number of sites and servers[^od-pricing] | | Plans | Free €0: 3 monitors · Pro €29: 20 · Team €79: 100 · Business €199: 300 | Solo $17: 2 sites · Freelance $55: 10 · Studio $109: 25 · Portfolio $275: 100 · Scale $439: 200, and steps in between[^od-pricing] | | Sample app (below) | €29 | $55, plus a tool for errors and requests | | Company | EU | Immutable VOF, Berlaar, Belgium[^od-terms] | | Data location | EU (Hetzner) | Belgium (Combell); performance metrics on ClickHouse Cloud, region not stated[^od-security] | | Uptime interval | 30 s to 1 hour, depending on plan | Every minute, from 1 of 16 locations, verified from a second[^od-uptime] | | SSL, domain, DNS | Yes | Yes[^od-pricing] | | Cron | Schedule worked out from the runs your app reports | Ping URL per task, or a Laravel package that syncs your schedule; 5 tasks per monitor[^od-cron] | | Broken links, mixed content | Yes, daily (Pro and up) | Yes, daily[^od-freq] | | Performance, Lighthouse, ping, TCP | No | Yes[^od-pricing] | | Status pages | Up to 10 per team (Pro and up) | Unlimited, with custom domains[^od-status] | | Errors, requests, queries, jobs, logs | Yes | Not listed[^od-pricing] | | Alerts | iOS push, email, Slack, Discord, Microsoft Teams, signed webhooks | Email plus 12 channels, including Slack, Teams, Discord, SMS, PagerDuty and webhooks[^od-notify] | | API / MCP | REST API; MCP server with OAuth 2.1 sign-in (read-only or read & write) or a personal token | API, MCP server with OAuth 2.1[^od-mcp] | | Free tier | Yes: 1 project, 3 monitors every 5 minutes | No; 10-day trial[^od-pricing] | | Self-hosting | No | Not listed[^od-pricing] | ## What does a typical app cost? The sample: 2 Laravel apps, 3M events a month in total, 5 people, 10 uptime monitors with SSL checks. - Lampwick Pro, €29: 5M events, 5 projects, unlimited seats, 20 monitors every minute with SSL, domain and DNS checks. - Oh Dear Freelance, $55: 10 sites, unlimited users, every check type.[^od-pricing] For exceptions and requests add application monitoring, such as Nightwatch Pro at $20.[^nw-pricing] Total: $75. For site checks alone, compare €29 with $55. Prices exclude VAT; we don't convert currencies. Lampwick's monitors are included in the plan, with no charge per monitor, and uptime checks don't count as events. ## Who wins on each topic ### Uptime: Oh Dear wins Oh Dear checks every minute from a location you choose and confirms a failure from a second one, out of 16.[^od-uptime] Lampwick checks from one place. Oh Dear also has ping and TCP checks; Lampwick checks URLs only. ### SSL, domain and DNS: tie Both check certificate expiry, domain expiry and DNS changes.[^od-pricing] Oh Dear checks more often by default: certificates every 30 minutes, DNS every 2 hours, domains every 4 hours.[^od-freq] Lampwick reads certificates every 6 to 12 hours, NS and MX records hourly and domains daily over RDAP. ### Cron: depends on how you run it Oh Dear gives each task a ping URL, so it works for any cron job on any server; `spatie/laravel-schedule-monitor` syncs a Laravel schedule. It includes 5 tasks per monitor.[^od-cron] Lampwick needs no ping URLs: it works out each task's cron from the runs your app reports, up to 500 per project, but can't see tasks outside your app. ### Broken links: tie Both crawl daily for broken links and mixed content.[^od-freq] Lampwick stops at 250 pages per crawl. ### Status pages: Oh Dear wins Oh Dear includes unlimited status pages on your own domain, and can post incident updates automatically.[^od-status] Lampwick has up to 10 per team at `app.lampwick.dev/status/`, with notices you write yourself. ### Inside your app: Lampwick wins Oh Dear tells you a site is down or slow. Lampwick also shows why: the exception, the slow route or query, the failed job and the log lines. ### Alerts: Oh Dear wins Oh Dear sends email and 12 other channels, including Slack, Teams, SMS, PagerDuty and webhooks.[^od-notify] Lampwick sends iOS push, email, Slack, Discord, Microsoft Teams and signed webhooks ([alert channels](/docs/alerts/#alert-channels)), but no SMS or PagerDuty. ### MCP: tie Oh Dear's MCP server uses OAuth 2.1, reads monitors and their check results and, with write access, manages monitors, status pages and maintenance windows.[^od-mcp] Lampwick's [MCP server](/docs/mcp/) also signs in with OAuth 2.1, or takes a personal token, and adds your app's issues, requests and logs. ## When to pick Oh Dear instead - You look after many client sites and need site checks, not app monitoring. - You want checks from several locations, or ping, TCP, performance and Lighthouse checks. - You need status pages on your own domain. - Alerts must reach PagerDuty or SMS. - Your cron jobs run outside your app. [Start free](https://app.lampwick.dev/signup). Also see [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/) and the [Nightwatch alternatives](/compare/nightwatch-alternatives/). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), prices in USD excluding VAT, verified on 2026-11-10. [^od-terms]: [Oh Dear terms](https://ohdear.app/terms), verified on 2026-11-10. [^od-security]: [Oh Dear security](https://ohdear.app/security), verified on 2026-11-10. [^od-uptime]: [Oh Dear uptime and performance monitoring docs](https://ohdear.app/docs/features/uptime-and-performance-monitoring), verified on 2026-11-10. [^od-cron]: [Oh Dear scheduled task monitoring](https://ohdear.app/features/scheduled-task-monitoring), [How many tasks can I monitor?](https://ohdear.app/docs/faq/how-many-tasks-can-i-monitor) and [schedule-monitor:sync FAQ](https://ohdear.app/docs/faq/why-does-schedule-monitorsync-change-the-expected-cron-expression), verified on 2026-11-10. [^od-freq]: [Oh Dear: how often are checks performed?](https://ohdear.app/docs/faq/how-often-are-checks-performed), default frequencies, verified on 2026-11-10. [^od-status]: [Oh Dear status pages](https://ohdear.app/features/status-pages), verified on 2026-11-10. [^od-notify]: [Oh Dear notification channels](https://ohdear.app/docs/notifications/notifications), verified on 2026-11-10. [^od-mcp]: [Oh Dear MCP server docs](https://ohdear.app/docs/tools-and-sdks/oh-dear-mcp-server), verified on 2026-11-10. [^nw-pricing]: [Nightwatch pricing](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. --- Source: https://lampwick.dev/compare/lampwick-vs-posthog/ # Lampwick vs PostHog for product analytics > PostHog is a product analytics suite with autocapture, replay and flags. Lampwick monitors your app and adds funnels from its requests. Verified 12 Nov 2026. We make Lampwick. Here's where each tool is the better pick. These are different tools that overlap in one place. PostHog is a product analytics suite: autocapture in the browser and mobile apps, events you name, session replay, feature flags and experiments, plus error tracking and logs. Lampwick is application monitoring (errors, requests, jobs, uptime) that also gives you active users, retention and funnels from the requests it already receives. If product analytics is the job, PostHog is the stronger tool. If monitoring is the job and you'd like to see what errors and deploys do to your signed-in users, Lampwick covers that without a second tool. Last reviewed 12 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes and verified on 2026-11-12. ## Data, analytics features and prices side by side | | Lampwick | PostHog | |---|---|---| | What it is | Application monitoring, with product analytics of signed-in users from the same data | Product analytics suite with replay, flags, experiments, surveys, error tracking and logs[^ph-pricing] | | How data gets in | The requests and exceptions your monitoring already sends | A JavaScript or mobile SDK with autocapture, or `capture` calls with event names you choose; server SDKs for Node.js, Python, PHP, Ruby, Go, Java and more[^ph-capture] | | Who is counted | Signed-in users only | Anonymous and identified users[^ph-capture] | | Funnel steps | Server routes, such as `POST /checkout`, and custom events your server sends; browser page views and events only as anonymous counts | Events, pageviews filtered by URL, or actions[^ph-funnels] | | Insights | Active users, stickiness, weekly retention, funnels, user paths, usage per route | Trends, funnels, paths, stickiness, lifecycle, retention, SQL[^ph-analytics] | | Errors next to conversion | Per funnel step: how users who hit an error or a slow response went on; conversion by deploy; users an issue hit and who came back | Error tracking is a separate product in the same platform[^ph-pricing] | | Session replay | No | Yes, 5K recordings a month free[^ph-pricing] | | Feature flags, experiments | No | Yes, 1M flag requests a month free[^ph-pricing] | | Uptime, SSL, domain, cron | Yes | Not on the pricing page[^ph-pricing] | | Price model | Per team per month, by events; analytics included ([all plans](/pricing/)) | Per product by usage; first 1M analytics events a month free, then from $0.00005 per event[^ph-pricing-md] | | History | Records 3 to 60 days by plan; user activity 30 days to 13 months | Events 1 year on free, 2 years on pay-as-you-go, longer with platform packages[^ph-pricing] | | Data location | EU | US or EU cloud[^ph-pricing] | | MCP | Yes, OAuth or a personal token | Yes, at `mcp.posthog.com`, sign in with your PostHog account[^ph-mcp] | ## How the data gets in PostHog's web SDK captures pageviews, clicks, inputs and form submissions by default, and you add `posthog.capture('user_signed_up')` for events of your own. Backend-only setups send events, including `$pageview`, with a server SDK.[^ph-capture] PostHog also has a PHP SDK and an experimental Laravel SDK.[^ph-php] By default Lampwick adds nothing to your app for analytics. The [Laravel package](/docs/laravel/), [OpenTelemetry](/docs/opentelemetry/) or [HTTP ingest](/docs/http-ingest/) already send each request with the signed-in user to monitor it, and Lampwick counts users, retention and funnels from those: nothing to name, nothing to keep in sync with your code. If you want more, send custom events from your server with the user id. The [browser script](/docs/browser/#custom-events-and-page-views) can count page views and custom events (`lampwick.track(...)`) too, as anonymous totals: no cookies, nothing stored in the browser, so [no consent banner for Lampwick's analytics](/legal/dpa/#analytics-and-your-users-devices). The cost of that: Lampwick only sees what reaches your server, plus what you send it on purpose. Clicks and scrolls are never captured on their own, and an anonymous visitor on your landing page only counts, never as a person. ## Who wins on each topic ### Anonymous visitors and marketing: PostHog wins PostHog counts anonymous and identified users, and its web SDK captures pageviews by default.[^ph-capture] Lampwick counts signed-in users only, because a request needs a user id to be followed to the next one. For a signup funnel that starts on a public page, use PostHog. ### Autocapture and custom events: PostHog wins PostHog autocaptures interactions on the web and in some mobile SDKs, and takes any event you name, with properties to break funnels down by.[^ph-autocapture] Lampwick takes custom events too, but only the ones you send, with up to 20 properties, followed per user only when your server sends them, and without breakdowns by property ([custom events and page views](/docs/funnels/#custom-events-and-page-views)). It has no autocapture. ### Session replay, feature flags and experiments: PostHog wins PostHog has all three, each with a monthly free allowance.[^ph-pricing] Lampwick has none of them. ### Funnel analysis: PostHog, for depth PostHog funnels break down by event and person properties and cohorts, and its correlation analysis highlights what affects conversion; from a funnel you can open the sessions of users who dropped off.[^ph-funnels] Lampwick funnels have 2 to 8 route steps with a conversion window, and per step the users, conversion, drop-off and median time. ### What errors and deploys do to users: Lampwick wins, for signed-in users This is where Lampwick's analytics come from: the same data as your errors. Per funnel step it compares users who hit a server error, an exception or a slow response with everyone else, lists the issues on the step that loses the most users and groups conversion by deploy. Every issue shows how many users it hit and how many came back ([impact of an issue](/docs/users/#impact-of-an-issue)). On Team and up, a [funnel alert](/docs/funnels/#funnel-alerts) tells you when a step's conversion drops, with the deploy and the top issue on that step. PostHog has error tracking in the same platform;[^ph-pricing] we haven't found these per-step error comparisons in its funnel docs.[^ph-funnels] ### Monitoring: Lampwick wins Lampwick tracks requests with p95, queued jobs, queries and logs, and checks uptime, SSL certificates, domains, DNS and the scheduler, with [status pages](/docs/status-pages/) from Pro. Uptime and cron checks aren't on PostHog's pricing page.[^ph-pricing] ### History: PostHog wins PostHog keeps events 1 year on its free plan and 2 years on pay-as-you-go.[^ph-pricing] Lampwick keeps a user's active days for 30 days on Free, 90 on Pro and 13 months on Team and Business, and the requests funnels are built from for 3 to 60 days. ### Price: depends on what you need PostHog's free tier covers 1M analytics events, 5K recordings and 1M flag requests a month, with 1 project; pay-as-you-go raises that to 6 projects.[^ph-pricing] Lampwick's analytics are part of every plan and count no extra events, because they read requests you already send; funnels per project are 1 on Free, 3 on Pro, 20 on Team and unlimited on Business. You'd pay Lampwick for monitoring, not for analytics. ### MCP: tie Both have a hosted MCP server. PostHog's runs HogQL queries, manages feature flags and experiments, and works with its error tracking.[^ph-mcp] Lampwick's has `findDropOffs`, `getFunnel`, `getRetention` and `getIssueImpact` next to the monitoring tools ([MCP](/docs/mcp/)). ## Using both They don't exclude each other. PostHog for the marketing site, anonymous visitors, replay and flags; Lampwick for errors, uptime and what an error or a deploy does to signed-in users in your app. ## When to pick PostHog instead - You need to count anonymous visitors, such as a landing page or signup funnel. - You want autocapture, events you analyze by their properties, session replay, feature flags or A/B tests. - You want more than a year of event history, or SQL on your events. - You need funnels on clicks or screens in a mobile app, or on anonymous visitors. Read how Lampwick does it: [users, active users and retention](/docs/users/), or [Start free](https://app.lampwick.dev/signup). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^ph-pricing]: [PostHog pricing](https://posthog.com/pricing), verified on 2026-11-12. [^ph-pricing-md]: [PostHog pricing, per-unit rates](https://posthog.com/pricing.md), verified on 2026-11-12. The first paid tier is $0.00005 per event for 1M to 2M events a month; identified events cost more. [^ph-capture]: [PostHog: capturing events](https://posthog.com/docs/product-analytics/capture-events), verified on 2026-11-12. [^ph-autocapture]: [PostHog autocapture](https://posthog.com/docs/product-analytics/autocapture), verified on 2026-11-12. [^ph-php]: [PostHog PHP SDK](https://posthog.com/docs/libraries/php), verified on 2026-11-12. [^ph-funnels]: [PostHog funnels](https://posthog.com/docs/product-analytics/funnels), verified on 2026-11-12. [^ph-analytics]: [PostHog product analytics docs](https://posthog.com/docs/product-analytics), verified on 2026-11-12. [^ph-mcp]: [PostHog MCP server](https://posthog.com/docs/model-context-protocol), verified on 2026-11-12. --- Source: https://lampwick.dev/compare/lampwick-vs-sentry/ # Lampwick vs Sentry for Laravel apps > Sentry covers more languages, longer history and session replay. Lampwick adds uptime, SSL, domain, DNS checks and status pages. Verified 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. Sentry is the stronger error tracker when your Laravel app is one of several services in different languages, or when you need session replay, profiling and 90 days of history. Lampwick is the better fit for a Laravel app that also needs uptime, certificate, domain, DNS and cron checks and a status page, on one bill. Last reviewed 10 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes and verified on 2026-11-10. ## Prices, limits and features side by side | | Lampwick | Sentry | |---|---|---| | Price model | Per team per month, by events | Per organization per month, by errors, spans, replays and logs, plus pay as you go[^se-pricing] | | Plans | Free; from €29: 5M events / 14 days ([all plans](/pricing/)) | Free; from $29: 50k errors, 5M spans, up to 90 days ([table below](#sentry-pricing-for-a-laravel-app))[^se-pricing] | | Company | NIO Media, Enschede, Netherlands (EU) | Functional Software, Inc., San Francisco, US; California law[^se-terms] | | Data location | EU | US (Iowa) or EU (Frankfurt)[^se-region] | | Laravel support | `lampwick/laravel`, Laravel 12 or 13 | `sentry/sentry-laravel`, Laravel 11 and later; older versions have their own guides[^se-laravel] | | Other languages | Any app over [HTTP ingest](/docs/http-ingest/), no SDKs | SDKs for about 20 platforms, from Python and Go to mobile and game engines[^se-platforms] | | Browser errors | Script tag, no source maps | JavaScript SDKs, session replay (50 replays included)[^se-pricing] | | Uptime checks | 3 to 300 monitors, every 30 s to 1 hour | 1 included, then $1.00 per uptime alert[^se-pricing] | | SSL, domain, DNS | Yes | Not on the pricing page[^se-pricing] | | Cron | Schedule learned from your app's runs, up to 500 tasks per project | Sentry Crons: 1 monitor included, then $0.78 per monitor[^se-pricing] | | Status pages, site crawl | Yes (Pro and up) | Not on the pricing page[^se-pricing] | | Alerts | iOS push, email, Slack, Discord, Microsoft Teams, signed webhooks | Email and Slack named in the docs[^se-alerts] | | MCP | Yes, OAuth or a personal token | Yes, at mcp.sentry.dev, OAuth[^se-mcp] | | Self-hosting | No | Not on the pricing page[^se-pricing] | ## Sentry for Laravel: what you get Install `sentry/sentry-laravel` with Composer and register it in `bootstrap/app.php`; it needs Laravel 11 or later.[^se-laravel] Sentry's Laravel page lists errors with local variables in the stack, tracing with automatic spans for views, cache, queries, Redis and the HTTP client, logs next to their traces, queued jobs, profiling from SDK 3.3.0 and scheduled-task monitoring through Sentry Crons.[^se-for-laravel] Lampwick's [Laravel package](/docs/laravel/) captures requests, queries, jobs, exceptions, logs and scheduled tasks with the MIT-licensed instrumentation of `laravel/nightwatch`, and sends them through a local ingest daemon. It needs Laravel 12 or 13. ## Sentry pricing for a Laravel app | Plan | Monthly | Billed yearly | Errors | Spans | Users | History | |---|---|---|---|---|---|---| | Developer | $0 | $0 | 5k | 5M | 1 | 30 days | | Team | $29 | $26 | 50k | 5M | Unlimited | Up to 90 days | | Business | $89 | $80 | 50k | 5M | Unlimited | Up to 90 days, plus sampled retention | Every plan includes 50 replays, 5 GB of logs, 1 uptime monitor and 1 cron monitor. Extras: logs $0.50 per GB, cron monitors $0.78 each, uptime $1.00 per uptime alert. The page doesn't give a per-unit price for extra errors or spans.[^se-pricing] The sample: 2 Laravel apps, 3M events a month, 5 people, 10 uptime monitors with SSL checks. - Lampwick Pro, €29: 5M events, 5 projects, unlimited seats, 20 monitors every minute with SSL, domain and DNS checks. - Sentry Team, $29: 50k errors and 5M spans for unlimited users. 1 uptime monitor is included; the page prices the others per uptime alert, so we can't compute 10 monitors.[^se-pricing] The units differ too: Sentry counts errors and spans, Lampwick counts every record. Lampwick counts one event per record your app sends (a request, query, job, log entry or exception). Uptime checks aren't events (monitors have their own limit per plan), and SSL, domain and DNS checks, alerts and schedule health count toward no limit. ## Nightwatch vs Sentry Both are common picks for Laravel; neither checks your site from outside. Nightwatch is Laravel-only and hosted by Laravel, $20 a month for 7.5M events and 30 days, with unlimited apps and seats.[^nw-pricing] Sentry is $29 for 50k errors and 5M spans, and works across languages and the browser.[^se-pricing] Pick Nightwatch for one Laravel stack and the most events per dollar; pick Sentry when you also run other services or need frontend errors with replay. Our [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/) page covers the Nightwatch side in detail. ## Who wins on each topic ### Errors and the browser: Sentry wins Sentry has SDKs for about 20 platforms,[^se-platforms] session replay and profiling.[^se-for-laravel] Lampwick groups exceptions into [issues](/docs/issues/) by class, file and line, with the stack trace, the request or job and the users it hit, and catches [browser errors](/docs/browser/) with a script tag, but without source maps or framework SDKs. ### History: Sentry wins Sentry keeps up to 90 days on Team and Business.[^se-pricing] Lampwick keeps 14 days on Pro, 30 on Team and 60 on Business. ### Uptime, SSL, domain and DNS: Lampwick wins Lampwick includes 20 to 300 [uptime monitors](/docs/uptime/) depending on plan, each with [SSL, domain and DNS checks](/docs/host-checks/). Sentry includes 1 uptime monitor and doesn't list certificate or domain checks.[^se-pricing] ### Cron: Lampwick, for Laravel Lampwick works out each task's schedule from the runs your app reports and alerts on missed, failing and slow runs, with no ping URLs to set up ([schedule health](/docs/cron-health/)). Sentry Crons works for any job on any server, but each monitor past the first costs $0.78.[^se-pricing] ### Status pages and broken links: Lampwick wins From Pro, Lampwick has up to 10 [status pages](/docs/status-pages/) per team and a daily [crawl](/docs/crawler/) for broken links and mixed content. Neither is on Sentry's pricing page.[^se-pricing] ### MCP: tie Both run a hosted MCP server with OAuth sign-in. Sentry's searches errors, analyzes performance and triages issues.[^se-mcp] Lampwick's [MCP server](/docs/mcp/) covers issues, requests, jobs, logs and monitors, read-only or read & write. ### Company and data: depends on what you need Sentry can store data in Frankfurt.[^se-region] The contract is with a US company under California law.[^se-terms] Lampwick is run by NIO Media, an EU company under EU jurisdiction. ## Switching from Sentry 1. Install the package and start the ingest daemon ([Laravel docs](/docs/laravel/)): ```bash composer require lampwick/laravel echo LAMPWICK_TOKEN=YOUR_PROJECT_TOKEN >> .env php artisan lampwick:agent ``` `YOUR_PROJECT_TOKEN` is on the project's **Settings** page. 2. Run both for a few days and compare the issues. 3. Remove `sentry/sentry-laravel` and its hook in `bootstrap/app.php`. 4. Add your uptime monitors under **Uptime**. ## When to pick Sentry instead - Your Laravel app is one of several services in other languages. - You need session replay, profiling or source-mapped frontend errors. - You want 90 days of history. - You run Laravel 11 or older. Comparing more tools? See [Sentry alternatives](/compare/sentry-alternatives/) and the [Laravel monitoring tools](/compare/laravel-monitoring-tools/) list. [Start free](https://app.lampwick.dev/signup), or read about [issues in Lampwick](/product/#issues). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^se-pricing]: [Sentry pricing](https://sentry.io/pricing/), verified on 2026-11-10. [^se-terms]: [Sentry terms of service](https://sentry.io/terms/), verified on 2026-11-10. [^se-region]: [Sentry data storage location](https://docs.sentry.io/organization/data-storage-location/), verified on 2026-11-10. [^se-laravel]: [Sentry for Laravel docs](https://docs.sentry.io/platforms/php/guides/laravel/), verified on 2026-11-10. [^se-for-laravel]: [Sentry for Laravel](https://sentry.io/for/laravel/), verified on 2026-11-10. [^se-platforms]: [Sentry platforms](https://docs.sentry.io/platforms/), verified on 2026-11-10. [^se-alerts]: [Sentry notifications docs](https://docs.sentry.io/product/alerts/notifications/), verified on 2026-11-10. Other integrations may exist; the page names only email and Slack. [^se-mcp]: [Sentry MCP server](https://mcp.sentry.dev/), verified on 2026-11-10. [^nw-pricing]: [Nightwatch pricing](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. --- Source: https://lampwick.dev/compare/laravel-monitoring-tools/ # Best Laravel monitoring tools (2026) > 13 Laravel monitoring tools compared: Nightwatch, Flare, Sentry, Pulse, Oh Dear, Inspector, Lampwick and more. Prices and limits verified on 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. For most Laravel apps the answer is Laravel Nightwatch: first-party, $20 a month for 7.5M events and 30 days of history.[^nw-pricing] It watches the app from inside and nothing else. Pick a different tool when you also need uptime and certificate checks, other languages, frontend errors, or your data in your own database. Last reviewed 10 Nov 2026. Every competitor fact links to the vendor's own page in the footnotes, verified on 2026-11-10. Prices exclude VAT; we don't convert currencies. ## All 13 tools in one table | Tool | Best for | Free option | Entry paid plan | Uptime checks | Self-host | |---|---|---|---|---|---| | Nightwatch | Laravel app monitoring, most events per dollar | 300k events, 14 days[^nw-pricing] | Pro $20: 7.5M events, 30 days[^nw-pricing] | No[^nw-docs] | No[^nw-pricing] | | Telescope, Pulse | Debugging locally; a basic production dashboard | Open source, runs in your app | – | No | Yes, in your app | | Flare | Laravel, PHP and frontend errors | 10-day trial[^fl-pricing] | Hobby €9: 10k errors, 1 project[^fl-pricing] | No[^fl-home] | No | | Sentry | Laravel next to other languages | 5k errors, 1 user[^se-pricing] | Team $29: 50k errors, 5M spans[^se-pricing] | 1 included[^se-pricing] | Not on the pricing page | | Oh Dear | Site checks for many sites | 10-day trial[^od-pricing] | Solo $17: 2 sites[^od-pricing] | Yes, every minute[^od-uptime] | No | | Inspector | PHP frameworks beyond Laravel | 30k transactions, 3 days[^in-pricing] | Beginner €15: 300k transactions[^in-pricing] | Not listed[^in-home] | No | | Honeybadger | Errors plus a few monitors | 5k errors, 1 monitor[^hb-pricing] | Team $26: 50k errors, 5 monitors[^hb-pricing] | 1 to 5 monitors[^hb-pricing] | No | | AppSignal | APM with logs, several languages | 30k requests[^as-pricing] | €18.25 a month billed yearly: 250k requests[^as-pricing] | Listed, count not stated[^as-pricing] | No | | Better Stack | Uptime, incidents and logs | 10 monitors and heartbeats[^bs-pricing] | $34 per responder seat[^bs-pricing] | Yes, up to every 30 s[^bs-pricing] | No | | NightOwl | Nightwatch data in your own PostgreSQL | 14-day trial[^no-pricing] | Hobby $5: 1 app, no event limit[^no-pricing] | No[^no-home] | Your database, hosted dashboard[^no-home] | | Laritor | Nightwatch-style monitoring, pay as you go | 300k events, 14 days[^lt-home] | $20: 20M events, 30 days[^lt-home] | Not listed[^lt-home] | Not stated | | Vigilant | Site checks for agencies, open source | 14-day trial[^vg-pricing] | Hobby €7.99: 2 sites[^vg-pricing] | Yes, several locations[^vg-home] | Yes, open source[^vg-home] | | Lampwick | App monitoring and site checks in one tool | 500k events, 3 days, 3 monitors | Pro €29: 5M events, 14 days, 20 monitors | 3 to 300 monitors | No | ## Laravel Nightwatch Laravel's own hosted monitoring. It records requests, outgoing requests, jobs, queries, mail, notifications, commands, cache and scheduled tasks, and bills each as one event.[^nw-pricing] Plans run from Free ($0, 300k events, 14 days) to Business ($300, 180M events, 90 days), with unlimited applications, environments and seats on every plan. Data centers are in the US and the EU.[^nw-pricing] Alerts go to Slack and webhooks, issues to Linear; the docs have no pages on uptime, SSL or status pages.[^nw-docs] **Verdict:** the default for Laravel-only teams. Our full comparison, including how the event tiers work: [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/#nightwatch-pricing-explained). ## Telescope and Pulse Free, open-source, first-party packages you run inside your own app, and Laravel says it will keep supporting both.[^nw-pricing] Telescope records requests, exceptions, logs, queries, jobs, mail and more, and Laravel's docs call it a companion to your local development environment.[^telescope] Pulse shows aggregated slow requests, jobs, queries, exceptions, cache hits and server usage, stored in MySQL, MariaDB or PostgreSQL.[^pulse] Neither one sends alerts. **Verdict:** install Telescope locally, Pulse in production for a dashboard. Add a hosted tool for alerts. Our [Telescope and Pulse guide](/guides/telescope-pulse-in-production/) covers running them in production. ## Flare From Spatie in Belgium. Errors, performance and logs for Laravel and PHP, plus frontend errors from JavaScript, React, Vue and Svelte.[^fl-home] Plans are €9, €29, €69 and €279 a month, each with 30 days of history; Standard (€29) has 100k errors, 20M spans and 5M logs, unlimited projects and users.[^fl-pricing] Alerts go to email, Slack, SMS, Discord, Microsoft Teams and webhooks.[^fl-notify] No uptime checks. **Verdict:** the pick when errors come first and you want the team behind Ignition. More in [Lampwick vs Flare](/compare/lampwick-vs-flare/). ## Sentry Error tracking and tracing for most languages, with the `sentry/sentry-laravel` package for Laravel 11 and later.[^se-laravel] Its Laravel page covers errors, tracing, logs, queued jobs, profiling and scheduled tasks through Sentry Crons.[^se-for-laravel] Developer is free for 1 user and 5k errors; Team is $29 a month ($26 billed yearly) with 50k errors, 5M spans and unlimited users. Every plan includes 1 uptime and 1 cron monitor.[^se-pricing] **Verdict:** the pick when your Laravel app sits next to services in other languages. More in [Lampwick vs Sentry](/compare/lampwick-vs-sentry/) and [Sentry alternatives](/compare/sentry-alternatives/). ## Oh Dear From Belgium, it watches sites from outside: uptime, broken links, mixed content, performance, Lighthouse, scheduled tasks, SSL, DNS and domain expiry, on every plan.[^od-pricing] It checks every minute and confirms a failure from a second location out of 16.[^od-uptime] Plans are priced by number of sites, from Solo ($17, 2 sites) to Scale ($439, 200 sites).[^od-pricing] It doesn't record exceptions or requests from inside your app. **Verdict:** the pick for agencies with many client sites. More in [Lampwick vs Oh Dear](/compare/lampwick-vs-oh-dear/). ## Inspector APM for Laravel, Symfony, CakePHP, CodeIgniter and Drupal, priced by transactions.[^in-pricing] It records requests, jobs and commands with their queries, cache commands and external calls, and has an MCP server for coding agents.[^in-home] Free has 30k transactions and 3 days; paid plans run from Beginner (€15, 300k, 7 days) to Enterprise (€169, 6M, 21 days).[^in-pricing] **Verdict:** the pick for PHP teams with more than one framework. ## Honeybadger Error tracking with uptime monitors, check-ins for scheduled tasks and status pages. The `honeybadger-io/honeybadger-laravel` package supports Laravel 5 through 12, and scheduled commands can ping a check-in.[^hb-laravel] Developer is free (5k errors, 15 days, 1 monitor every 5 minutes); Team is $26 a month with 50k errors, 90 days and 5 monitors every 2 minutes.[^hb-pricing] **Verdict:** errors, uptime monitors, check-ins and status pages in one tool when a handful of monitors is enough, and the pick for older Laravel versions. ## AppSignal APM, logs and uptime monitoring for several languages. Its PHP package auto-instruments Laravel and Symfony.[^as-php] Free has 30k requests; the paid plan shown starts at €18.25 a month billed yearly (€219) for 250k requests.[^as-pricing] **Verdict:** the pick if your team also runs Ruby, Elixir or Node.js apps and wants one APM. ## Better Stack Uptime monitoring, heartbeats, status pages, incident management, logs and error tracking, with data in Europe, the US or Singapore. The free tier, for personal projects, has 10 monitors and heartbeats, 1 status page and 100k exceptions. Paid use is $34 per responder seat a month ($29 billed yearly) plus usage.[^bs-pricing] **Verdict:** the pick when on-call and incident management matter more than Laravel depth. ## NightOwl Uses the same `laravel/nightwatch` package for capture, then writes every record to a PostgreSQL database you provide; its hosted dashboard reads from there.[^no-home] Plans are $5 (1 app, 14-day dashboard window), $15 (3 apps, 5 people per team) and $69 (unlimited), billed monthly, with no event limits. You pay for the database separately.[^no-pricing] **Verdict:** the pick if you want flat pricing and your data in your own database, and you're happy to run PostgreSQL. ## Laritor An independent Laravel monitoring service: exceptions, failed jobs, slow requests, queries, logs and health checks, with an MCP server and alerts to Slack, Discord, email, Teams, Telegram, GitHub, Linear and webhooks. The free trial has 300k events and 14 days; the paid plan is $20 a month for 20M events and 30 days, then $0.20 per 100k events.[^lt-home] **Verdict:** close to Nightwatch on price, with more alert channels. ## Vigilant Open-source website monitoring from Europe, hosted or on your own server. It covers uptime from several locations, user flows in real browsers, broken links, Lighthouse, DNS, certificates and CVEs, with client dashboards and PDF reports.[^vg-home] An optional Laravel healthcheck package reports when the scheduler or queue stops.[^vg-laravel] Hosted plans are €7.99 (2 sites), €29.99 (10) and €79.99 (50) a month.[^vg-pricing] **Verdict:** the pick for agencies who want Oh Dear-style checks plus browser flows, or to run it themselves. ## Lampwick Errors, requests, queries, jobs, scheduled tasks and logs from your app, plus uptime, SSL, domain and DNS checks, and status pages and a link crawler from Pro. The [Laravel package](/docs/laravel/) (Laravel 12 or 13) uses the MIT-licensed instrumentation of `laravel/nightwatch`; any other app can send records over [HTTP ingest](/docs/http-ingest/). Free is 500k events and 3 days; Pro is €29 for 5M events, 14 days and 20 monitors every minute. It's run by NIO Media in Enschede, the Netherlands: an EU company under EU jurisdiction. **Verdict:** the pick when you want one tool and one bill for what happens inside your app and what your visitors see. It loses to Nightwatch on events and history per euro, checks uptime from one location only, has no SMS or PagerDuty alerts, and catches [browser errors](/docs/browser/) without source maps. See [error tracking](/product/#issues) and [uptime monitoring](/product/#uptime). ## Which ones are Laravel APM tools? Application performance monitoring means tracing each request or job with its queries and outgoing calls. Nightwatch, Inspector, AppSignal, NightOwl, Laritor and Lampwick do that for Laravel; Sentry and Flare do it through spans. Pulse shows slow requests and queries in aggregate, without a timeline per request. Oh Dear, Vigilant and Better Stack's uptime product look at your site from outside, so they see that it's slow, not why. ## How to choose - Only Laravel application monitoring, best price: Nightwatch. - Free and in your own database: Telescope locally, Pulse in production. - Errors first, including the browser: Flare or Sentry. - Several languages: Sentry or AppSignal. - Many client sites: Oh Dear or Vigilant. - On-call and incidents: Better Stack. - App monitoring and site checks together: Lampwick, or Honeybadger for errors and a few monitors. [Start free](https://app.lampwick.dev/signup), or read the [Nightwatch alternatives](/compare/nightwatch-alternatives/). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^nw-pricing]: [Nightwatch pricing and FAQ](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. [^nw-docs]: [Nightwatch docs index](https://nightwatch.laravel.com/docs/llms.txt), verified on 2026-11-10: no pages on uptime, SSL, domain, DNS, status pages or link checks; integrations are Slack, Linear and webhooks. [^telescope]: [Laravel Telescope docs (13.x)](https://laravel.com/framework/docs/13.x/telescope), verified on 2026-11-10. [^pulse]: [Laravel Pulse docs](https://laravel.com/framework/docs/12.x/pulse), verified on 2026-11-10. [^fl-pricing]: [Flare pricing](https://flareapp.io/pricing), verified on 2026-11-10. [^fl-home]: [Flare home page](https://flareapp.io/), verified on 2026-11-10. [^fl-notify]: [Flare notification docs](https://flareapp.io/docs/notifications/configuring-notifications), verified on 2026-11-10. [^se-pricing]: [Sentry pricing](https://sentry.io/pricing/), verified on 2026-11-10. [^se-laravel]: [Sentry for Laravel docs](https://docs.sentry.io/platforms/php/guides/laravel/), verified on 2026-11-10. [^se-for-laravel]: [Sentry for Laravel](https://sentry.io/for/laravel/), verified on 2026-11-10. [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), USD excluding VAT, verified on 2026-11-10. [^od-uptime]: [Oh Dear uptime monitoring docs](https://ohdear.app/docs/features/uptime-and-performance-monitoring), verified on 2026-11-10. [^in-pricing]: [Inspector pricing](https://inspector.dev/pricing/), verified on 2026-11-10. [^in-home]: [Inspector home page](https://inspector.dev/), verified on 2026-11-10. [^hb-pricing]: [Honeybadger plans](https://www.honeybadger.io/plans/), verified on 2026-11-10. [^hb-laravel]: [Honeybadger for Laravel docs](https://docs.honeybadger.io/lib/php/integration/laravel/), verified on 2026-11-10. [^as-pricing]: [AppSignal plans](https://www.appsignal.com/plans), verified on 2026-11-10. [^as-php]: [AppSignal for PHP docs](https://docs.appsignal.com/php.html), verified on 2026-11-10. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), verified on 2026-11-10. [^no-home]: [NightOwl home page](https://usenightowl.com/), verified on 2026-11-10. [^no-pricing]: [NightOwl pricing](https://usenightowl.com/pricing/), verified on 2026-11-10. [^lt-home]: [Laritor home page and pricing](https://laritor.com/#pricing), verified on 2026-11-10. [^vg-home]: [Vigilant home page](https://govigilant.io/), verified on 2026-11-10. [^vg-pricing]: [Vigilant pricing](https://govigilant.io/pricing), verified on 2026-11-10. [^vg-laravel]: [Vigilant for Laravel](https://govigilant.io/platforms/laravel), verified on 2026-11-10. --- Source: https://lampwick.dev/compare/nightwatch-alternatives/ # Laravel Nightwatch alternatives (2026) > What Nightwatch doesn't do, what a typical app costs elsewhere, and the closest alternatives: NightOwl, Laritor, Flare, Sentry, Vigilant, Lampwick. We make Lampwick. Here's where each tool is the better pick. Nightwatch itself is hard to beat on price for Laravel application monitoring: Pro is $20 a month for 7.5M events and 30 days of history, with unlimited apps and seats.[^nw-pricing] This page covers why people still look elsewhere and the closest alternatives. Every Laravel monitoring tool, from Telescope to Oh Dear, is in [the best Laravel monitoring tools](/compare/laravel-monitoring-tools/); our side-by-side is at [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/). Last reviewed 10 Nov 2026. Competitor facts come from the vendor's own pages, linked in the footnotes and verified on 2026-11-10. ## Why look for a Nightwatch alternative? Nightwatch records what happens inside a Laravel app. What it doesn't do: - **No checks from outside.** No uptime, certificate, domain or DNS checks in its docs, so a site that's down before Laravel boots goes unnoticed.[^nw-docs] - **Laravel only.** Apps in other languages need a second tool.[^nw-pricing] - **Your data stays in Laravel's cloud.** You can pick a US or EU region, but not your own database.[^nw-pricing] - **No status pages** in its docs.[^nw-docs] - **Price at volume.** Extra events are opt-in; without a limit set, ingestion pauses for the rest of the period once you pass your included events.[^nw-extra] ## What would a typical app cost? The sample: 2 Laravel apps, 3M events a month in total, 5 people, 10 uptime monitors with SSL checks. Prices exclude VAT; we don't convert currencies. | Tool | Monthly price | How we got there | |---|---|---| | Nightwatch | $20 + uptime tool | Pro: 7.5M events, unlimited apps and seats. No uptime checks.[^nw-pricing] | | NightOwl | $15 + database + uptime tool | Team: up to 3 apps and 5 people, no event limit. You host PostgreSQL.[^no-pricing] | | Laritor | $20 + uptime tool | 20M events included, 30 days.[^lt-home] | | Lampwick | €29 | Pro: 5M events, 5 projects, unlimited seats, 20 monitors every minute. | | Flare | €29 + uptime tool | Standard: 100k errors, 20M spans, 5M logs, unlimited projects and users. Counts spans, not events.[^fl-pricing] | | Oh Dear (site checks only) | $55 + app monitoring | Freelance: 10 sites.[^od-pricing] | | Better Stack (uptime only) | $0 | Free tier: 10 monitors and heartbeats, for personal projects.[^bs-pricing] | | Sentry | Not computed | Team is $29 with 1 uptime monitor; the page prices extras "per uptime alert", which we can't turn into a cost for 10 monitors.[^se-pricing] | | Inspector | Not computed | Priced per transaction; we can't map 3M events to transactions without knowing your traffic.[^in-pricing] | | Honeybadger | Not computed | Team and Business include 5 monitors; the price for 10 isn't on the page.[^hb-pricing] | Lampwick counts one event per record your app sends (a request, query, job, log entry or exception). Uptime checks aren't events (monitors have their own limit per plan), and SSL, domain and DNS checks, alerts and schedule health count toward no limit. ## The closest alternatives ### NightOwl Uses `laravel/nightwatch` for capture and writes the records to a PostgreSQL database you provide; its hosted dashboard reads from there.[^no-home] Hobby is $5 for 1 app and Team $15 for 3 apps, with no event limits; you pay for PostgreSQL separately.[^no-pricing] Pick it for flat pricing and data in your own database. ### Laritor An independent Laravel monitoring service with exceptions, slow requests and queries, failed jobs, logs and an MCP server. $20 a month buys 20M events and 30 days, then $0.20 per 100k, and alerts go to more channels than Nightwatch's, including Telegram, GitHub and Linear.[^lt-home] ### Flare From Spatie in Belgium: errors, performance and logs for Laravel and PHP, plus frontend errors from JavaScript, React, Vue and Svelte.[^fl-home] From €9 a month with 30 days of history; no uptime checks.[^fl-pricing] More in [Lampwick vs Flare](/compare/lampwick-vs-flare/). ### Sentry SDKs for about 20 platforms[^se-platforms] and `sentry/sentry-laravel` for Laravel 11 and later.[^se-laravel] Team is $29 a month with 50k errors and 5M spans.[^se-pricing] The pick when Laravel sits next to other languages; more in [Lampwick vs Sentry](/compare/lampwick-vs-sentry/). ### Vigilant Open-source website monitoring from Europe: uptime from several locations, user flows in real browsers, broken links, Lighthouse, DNS and certificates.[^vg-home] It doesn't track exceptions or requests, so it adds to Nightwatch rather than replacing it. Hosted plans start at €7.99 a month for 2 sites.[^vg-pricing] ### Lampwick Errors, requests, queries, jobs and logs from your app, plus uptime, SSL, domain and DNS checks and [schedule health](/docs/cron-health/), with status pages and a link crawler from Pro. Its [Laravel package](/docs/laravel/) builds on `laravel/nightwatch`'s MIT-licensed instrumentation; Pro is €29 for 5M events and 14 days. It gives less volume and history per euro than Nightwatch, and has no SMS or PagerDuty alerts. Telescope, Pulse, Inspector, Better Stack, Honeybadger, AppSignal and Oh Dear are compared in [the best Laravel monitoring tools](/compare/laravel-monitoring-tools/). ## How to choose - Only application monitoring for Laravel: Nightwatch, or Laritor. - Your data in your own database, flat price: NightOwl. - Errors first, including the browser: Flare or Sentry. - Site checks for many client sites, next to Nightwatch: Vigilant or [Oh Dear](/compare/lampwick-vs-oh-dear/). - App monitoring and site checks together: Lampwick. [Start free](https://app.lampwick.dev/signup), see [all Laravel monitoring tools](/compare/laravel-monitoring-tools/), or compare in detail: [Nightwatch](/compare/lampwick-vs-nightwatch/), [Flare](/compare/lampwick-vs-flare/), [Sentry](/compare/lampwick-vs-sentry/), [Oh Dear](/compare/lampwick-vs-oh-dear/). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^nw-pricing]: [Nightwatch pricing and FAQ](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. [^nw-docs]: [Nightwatch docs index](https://nightwatch.laravel.com/docs/llms.txt), verified on 2026-11-10: no pages on uptime, SSL, domain, DNS, status pages or link checks. [^nw-extra]: [Nightwatch additional events docs](https://nightwatch.laravel.com/docs/additional-events.md), verified on 2026-11-10. [^no-home]: [NightOwl home page](https://usenightowl.com/), verified on 2026-11-10. [^no-pricing]: [NightOwl pricing](https://usenightowl.com/pricing/), verified on 2026-11-10. [^lt-home]: [Laritor home page and pricing](https://laritor.com/#pricing), verified on 2026-11-10. [^fl-pricing]: [Flare pricing](https://flareapp.io/pricing), verified on 2026-11-10. [^fl-home]: [Flare home page](https://flareapp.io/), verified on 2026-11-10. [^se-pricing]: [Sentry pricing](https://sentry.io/pricing/), verified on 2026-11-10. [^se-platforms]: [Sentry platforms](https://docs.sentry.io/platforms/), verified on 2026-11-10. [^se-laravel]: [Sentry for Laravel docs](https://docs.sentry.io/platforms/php/guides/laravel/), verified on 2026-11-10. [^in-pricing]: [Inspector pricing](https://inspector.dev/pricing/), verified on 2026-11-10. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), verified on 2026-11-10. [^hb-pricing]: [Honeybadger plans](https://www.honeybadger.io/plans/), verified on 2026-11-10. [^vg-home]: [Vigilant home page](https://govigilant.io/), verified on 2026-11-10. [^vg-pricing]: [Vigilant pricing](https://govigilant.io/pricing), verified on 2026-11-10. [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), verified on 2026-11-10. --- Source: https://lampwick.dev/compare/sentry-alternatives/ # Sentry alternatives (2026), with picks for Laravel > 10 Sentry alternatives: Rollbar, GlitchTip, Bugsink, Honeybadger, Better Stack, plus Flare, Nightwatch and Lampwick for Laravel. Verified 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. Sentry is hard to replace if you run many languages: it has SDKs for about 20 platforms, session replay and profiling, and Team is $29 a month for 50k errors and 5M spans.[^se-pricing] [^se-platforms] People look elsewhere for a lower price at volume, a Sentry-compatible tool they can run themselves, EU ownership, or a tool built for one framework. For Laravel and PHP teams there are specialist options below. Last reviewed 10 Nov 2026. Every competitor fact links to the vendor's own page in the footnotes, verified on 2026-11-10. Prices exclude VAT; we don't convert currencies. ## All 10 tools in one table | Tool | Best for | Free option | Entry paid plan | Works with Sentry SDKs | Self-host | |---|---|---|---|---|---| | Rollbar | Error tracking across languages | 5k occurrences, 30 days[^rb-pricing] | Not shown on the page[^rb-pricing] | No | No | | GlitchTip | A simpler, open-source Sentry | 1k events, 30 days[^gt-pricing] | Small $15: 100k events, 90 days[^gt-pricing] | Yes[^gt-home] | Yes[^gt-pricing] | | Bugsink | Sentry-compatible, Dutch company | 15k events, evaluation[^bk-home] | €16 for 75k events[^bk-home] | Yes[^bk-home] | Yes, free[^bk-home] | | Honeybadger | Errors plus uptime and check-ins | 5k errors, 15 days[^hb-pricing] | Team $26: 50k errors, 90 days[^hb-pricing] | No | No | | AppSignal | APM and logs, several languages | 30k requests[^as-pricing] | €18.25 a month billed yearly[^as-pricing] | No | No | | Better Stack | Errors with uptime and on-call | 100k exceptions[^bs-pricing] | $34 per responder seat[^bs-pricing] | Not checked | No | | Flare | Laravel and PHP errors | 10-day trial[^fl-pricing] | Hobby €9: 10k errors[^fl-pricing] | No | No | | Nightwatch | Laravel app monitoring | 300k events, 14 days[^nw-pricing] | Pro $20: 7.5M events[^nw-pricing] | No | No | | Inspector | PHP APM beyond Laravel | 30k transactions[^in-pricing] | Beginner €15: 300k[^in-pricing] | No | No | | Lampwick | App errors plus site checks | 500k events, 3 days | Pro €29: 5M events, 14 days | No | No | ## For any stack ### Rollbar Error tracking across languages. The free plan is $0 forever with 5,000 occurrences a month and 30 days of history; paid plans keep 90 (Essentials) or 180 days (Advanced), but the page shows no prices for them.[^rb-pricing] **Verdict:** worth a look if you want longer history than Sentry's 90 days. ### GlitchTip Open source and "compatible with Sentry client SDKs", with errors, performance, uptime checks and logs.[^gt-home] Hosted plans are $15 (100k events), $50 (500k) and $250 (3M) a month; you can also run it on your own server.[^gt-pricing] **Verdict:** the pick if you want to keep your Sentry SDKs and pay less, or run it yourself. ### Bugsink Point your existing Sentry SDKs at a new DSN and it works. It's run by a Dutch company with data in the EU, hosted from €16 a month for 75k events, or free to run yourself.[^bk-home] **Verdict:** the pick for an EU-run, Sentry-compatible error tracker. ### Honeybadger Error tracking with uptime monitors, check-ins and status pages. Developer is free (5k errors, 15 days, 1 monitor); Team is $26 with 50k errors, 90 days and 5 monitors every 2 minutes; Business is $80 with 180 days.[^hb-pricing] **Verdict:** the pick for a small team that wants errors and a few uptime checks in one tool. ### AppSignal APM, errors, logs and uptime for Ruby, Elixir, Node.js, Python, Go, Java, Rust and PHP. The paid plan shown is €219 a year (€18.25 a month) for 250k requests.[^as-pricing] **Verdict:** the pick when you want APM and errors together across a few languages. ### Better Stack Error tracking next to uptime, status pages, on-call and logs. The free tier has 100k exceptions, 10 monitors and 1 status page, for personal projects; extra exceptions cost $0.000050 each with data in Europe.[^bs-pricing] **Verdict:** the pick when incident management and on-call matter as much as errors. ## For Laravel and PHP teams Sentry's `sentry/sentry-laravel` package works well and supports Laravel 11 and later.[^se-laravel] These tools trade breadth across languages for more Laravel detail, or a lower price. ### Flare Built by Spatie in Belgium for Laravel, PHP and JavaScript, with first-class support for queues, jobs and Livewire.[^fl-home] Standard is €29 a month for 100k errors, 20M spans and 5M logs, with unlimited projects and users.[^fl-pricing] **Verdict:** the closest Laravel-first replacement for Sentry's error tracking. More in [Lampwick vs Flare](/compare/lampwick-vs-flare/). ### Laravel Nightwatch First-party from Laravel. Pro is $20 a month for 7.5M events (requests, queries, jobs, exceptions and more) and 30 days, with unlimited apps and seats.[^nw-pricing] Laravel only, no frontend errors. **Verdict:** the cheapest way to see everything inside a Laravel app. See [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/). ### Inspector Transaction-based APM for Laravel, Symfony, CakePHP, CodeIgniter and Drupal, from €15 a month for 300k transactions and 7 days.[^in-pricing] **Verdict:** the pick for PHP teams with several frameworks. ### Lampwick Exceptions grouped into [issues](/docs/issues/), plus requests, queries, jobs and logs, from Laravel through its package or any stack over OpenTelemetry, and uptime, SSL, domain and DNS checks, [schedule health](/docs/cron-health/) and status pages (Pro and up). Pro is €29 for 5M events, 14 days and 20 monitors. It falls short of Sentry on languages, history and the browser: [browser errors](/docs/browser/) come from a script tag, without source maps. **Verdict:** the pick when error tracking and site checks should be one tool on one plan. Details in [Lampwick vs Sentry](/compare/lampwick-vs-sentry/) and under [issues](/product/#issues) on the product page. ## How to choose - Many languages, replay and profiling: stay on Sentry. - Keep your Sentry SDKs, pay less or run it yourself: GlitchTip or Bugsink. - Errors with on-call: Better Stack. - Laravel only, most data per dollar: Nightwatch. - Laravel errors first: Flare. - Laravel errors and uptime, SSL and cron in one tool: Lampwick, or Honeybadger. More Laravel options in [the best Laravel monitoring tools](/compare/laravel-monitoring-tools/). [Start free](https://app.lampwick.dev/signup). ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^se-pricing]: [Sentry pricing](https://sentry.io/pricing/), verified on 2026-11-10. [^se-platforms]: [Sentry platforms](https://docs.sentry.io/platforms/), verified on 2026-11-10. [^se-laravel]: [Sentry for Laravel docs](https://docs.sentry.io/platforms/php/guides/laravel/), verified on 2026-11-10. [^rb-pricing]: [Rollbar pricing](https://rollbar.com/pricing/), verified on 2026-11-10. [^gt-pricing]: [GlitchTip pricing](https://glitchtip.com/pricing), verified on 2026-11-10. [^gt-home]: [GlitchTip home page](https://glitchtip.com/), verified on 2026-11-10. [^bk-home]: [Bugsink home page](https://www.bugsink.com/), verified on 2026-11-10. [^hb-pricing]: [Honeybadger plans](https://www.honeybadger.io/plans/), verified on 2026-11-10. [^as-pricing]: [AppSignal plans](https://www.appsignal.com/plans), verified on 2026-11-10. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), verified on 2026-11-10. [^fl-pricing]: [Flare pricing](https://flareapp.io/pricing), verified on 2026-11-10. [^fl-home]: [Flare home page](https://flareapp.io/), verified on 2026-11-10. [^nw-pricing]: [Nightwatch pricing](https://laravel.com/nightwatch/pricing), verified on 2026-11-10. [^in-pricing]: [Inspector pricing](https://inspector.dev/pricing/), verified on 2026-11-10. --- Source: https://lampwick.dev/compare/uptimerobot-alternatives/ # UptimeRobot alternatives (2026): 6 tools compared > UptimeRobot's free plan allows commercial use. Alternatives: Oh Dear, Better Stack, Healthchecks.io, Uptime Kuma, Pingdom, Lampwick. Verified 10 Nov 2026. We make Lampwick. Here's where each tool is the better pick. UptimeRobot is still hard to beat on price for plain uptime checks. Its free plan has 50 monitors every 5 minutes and says "Good for personal and commercial projects."[^ur-pricing] Look elsewhere if you need checks from several locations with confirmation, on-call, cron monitoring, a tool you run yourself, or your app's errors next to its uptime. Last reviewed 10 Nov 2026. Every competitor fact links to the vendor's own page in the footnotes, verified on 2026-11-10. Prices exclude VAT; we don't convert currencies. ## What UptimeRobot costs today | Plan | Price a month (billed yearly / monthly) | Monitors | Interval | Status pages | Seats | |---|---|---|---|---|---| | Free | €0 | 50 | 5 min | 1 basic | No login seats | | Solo | €9 / €10 | 10 or 50 | 60 s | 3, own domain | Not stated | | Team | €30 / €35 | 100 | 30 s | 100 | 3 | | Scale | €55 / €65 | 200 or 500 | 15 s | Unlimited | 5 | The free plan includes HTTP, port, ping, keyword and DNS monitors, but only 5 integrations, and keeps 3 months of data. Webhooks, Zapier and PagerDuty come with Team.[^ur-pricing] The page lists "SSL & Domain exp. monitor" on the free card, but its comparison table says SSL monitoring is in paid plans only. The page showed prices in euros when we checked.[^ur-pricing] ## All 6 tools in one table | Tool | Best for | Free option | Entry paid plan | Cron / heartbeat | Self-host | |---|---|---|---|---|---| | Oh Dear | Many sites, every check type | 10-day trial[^od-pricing] | Solo $17: 2 sites[^od-pricing] | Yes[^od-pricing] | No | | Better Stack | Uptime with on-call and incidents | 10 monitors and heartbeats, personal projects[^bs-pricing] | $34 per responder seat[^bs-pricing] | Heartbeats[^bs-pricing] | No | | Healthchecks.io | Cron jobs and background tasks only | 20 checks[^hc-pricing] | Business $20: 100 checks[^hc-pricing] | Yes, that's all it does[^hc-home] | Yes, open source[^hc-github] | | Uptime Kuma | Running it yourself, for free | Open source (MIT)[^uk-github] | – | Push monitors[^uk-github] | Only[^uk-github] | | Pingdom | Enterprise synthetics and real-user monitoring | Trial[^pd-pricing] | Not shown on the page[^pd-pricing] | Not checked | No | | Lampwick | Uptime plus your app's errors, jobs and logs | 3 monitors every 5 min | Pro €29: 20 monitors every minute | Laravel scheduled tasks | No | ## Oh Dear From Belgium. Every plan includes uptime, broken links and mixed content, performance and Lighthouse, ping and TCP, scheduled tasks, SSL, DNS and domain expiry; plans are priced by sites, from Solo ($17, 2 sites) to Scale ($439, 200 sites).[^od-pricing] It checks every minute and confirms a failure from a second location out of 16.[^od-uptime] **Verdict:** the pick for agencies and anyone who wants every check type, from several locations. More in [Lampwick vs Oh Dear](/compare/lampwick-vs-oh-dear/). ## Better Stack Uptime checks up to every 30 seconds, heartbeats, status pages, on-call and incident management, plus logs and error tracking. Free (for personal projects) has 10 monitors and heartbeats and 1 status page. Paid use is $34 per responder seat a month ($29 billed yearly), with 10 monitors included and $25 a month per 50 more.[^bs-pricing] **Verdict:** the pick when on-call schedules and incidents matter as much as the checks. ## Healthchecks.io Not an uptime monitor: it alerts when a cron job or background task stops pinging it.[^hc-home] Hobbyist is free for 20 checks; Business is $20 a month for 100 checks with SMS and phone calls.[^hc-pricing] It's open source under the BSD 3-clause license, so you can run it yourself.[^hc-github] **Verdict:** the pick to add cron monitoring next to UptimeRobot, for any language or server. ## Uptime Kuma A self-hosted monitor you run with Docker: HTTP, keyword, JSON query, TCP, ping, DNS, push and Docker container checks, 20-second intervals, more than 90 notification services and status pages on your own domain. MIT-licensed.[^uk-github] **Verdict:** the pick if you're happy to run and watch a server yourself. Remember that it can't tell you its own server is down. ## Pingdom SolarWinds Pingdom offers synthetic uptime checks, advanced transaction checks, SMS credits and real-user monitoring, in tiers from 10 to 30,000 uptime checks, with a trial.[^pd-pricing] **Verdict:** the pick for larger companies that want real-user monitoring from the same vendor. ## Lampwick [Uptime monitors](/docs/uptime/) every 30 seconds to 1 hour, each with [SSL, domain and DNS checks](/docs/host-checks/), and, from Pro, [status pages](/docs/status-pages/) and a daily [crawl](/docs/crawler/) for broken links and mixed content. What UptimeRobot doesn't do: connect your app and Lampwick also shows the exception, slow request, failed job and log lines behind an outage ([Laravel](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/)). Free is 3 monitors every 5 minutes; Pro is €29 a month for 20 monitors every minute; Team is €79 for 100 every 30 seconds. Where it falls short: the free plan is far smaller than UptimeRobot's, checks run from one location, URLs only (no ping, port or keyword checks), and there are no SMS or PagerDuty alerts. Cron monitoring covers Laravel scheduled tasks reported by your app, not ping URLs for any job. **Verdict:** the pick when you want uptime and your app's errors in one place. See [uptime monitoring](/product/#uptime) and [how to know when your app is down](/guides/know-when-your-app-is-down/). ## How to choose - The most free monitors, commercial use allowed: stay on UptimeRobot. - Every check type, from several locations: Oh Dear. - On-call and incidents: Better Stack. - Cron jobs on any server: Healthchecks.io. - Free and on your own server: Uptime Kuma. - Uptime plus your app's errors, jobs and logs: Lampwick. [Start free](https://app.lampwick.dev/signup): open **Uptime**, paste your URL and click **Add monitor**. ## Try it with your agent Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The agent creates a trial workspace without an account and connects your app. You claim it from the link it gives you and choose what the agent may keep: [how it works](/docs/agents/). [^ur-pricing]: [UptimeRobot pricing](https://uptimerobot.com/pricing/), verified on 2026-11-10. The free card reads "Good for personal and commercial projects. No credit card required!" [^od-pricing]: [Oh Dear pricing](https://ohdear.app/pricing), USD excluding VAT, verified on 2026-11-10. [^od-uptime]: [Oh Dear uptime monitoring docs](https://ohdear.app/docs/features/uptime-and-performance-monitoring), verified on 2026-11-10. [^bs-pricing]: [Better Stack pricing](https://betterstack.com/pricing), verified on 2026-11-10. [^hc-pricing]: [Healthchecks.io pricing](https://healthchecks.io/pricing/), verified on 2026-11-10. [^hc-home]: [Healthchecks.io home page](https://healthchecks.io/), verified on 2026-11-10. [^hc-github]: [healthchecks/healthchecks on GitHub](https://github.com/healthchecks/healthchecks), verified on 2026-11-10. [^uk-github]: [louislam/uptime-kuma on GitHub](https://github.com/louislam/uptime-kuma), verified on 2026-11-10. [^pd-pricing]: [Pingdom pricing](https://www.pingdom.com/pricing/), verified on 2026-11-10. --- Source: https://lampwick.dev/contact/ # Contact > Write to the people behind Lampwick about sales, support, billing, privacy or abuse. The form goes to our inbox, and we answer by email. Use the form below, or email [info@niomedia.nl](mailto:info@niomedia.nl). Both reach the same inbox. We reply to the email address you enter; the form sends no automatic confirmation.
Enter your name.
Enter one email address, like you@example.com.
Write your message. At most 5000 characters. For an abuse report, include the URL and why you think the content is illegal.

We use your message only to answer you, and keep it as long as other support mail. See the Privacy policy.

## Postal address NIO Media, Roomweg 68, 7523 BR Enschede, the Netherlands. Chamber of Commerce (KvK) 69855773. Lampwick is a service of NIO Media; the [Terms of service](/legal/terms/#who-we-are) have the details. --- Source: https://lampwick.dev/errors/ # Laravel and PHP errors, explained > The Laravel and PHP errors developers hit most, each with its real causes, how to tell which one you have, the fix in code and how to catch it. Find the error message you got. Its page lists the causes, most common first, with how to tell which one you have and the fix in code. The code runs on Laravel 12 and 13 with PHP 8.3 or later; where the two Laravel versions differ, the page says so. Every page ends with how to notice the error in production before a user reports it. To see all of your app's exceptions in one list, grouped and counted, see [issues](/docs/issues/). - [419 Page Expired and CSRF token mismatch in Laravel](/errors/419-page-expired/): Laravel returns 419 Page Expired, or "CSRF token mismatch." as JSON, when a POST has no valid CSRF token: no @csrf, an expired session or a cookie issue. - [Allowed memory size of 134217728 bytes exhausted](/errors/allowed-memory-size-exhausted/): PHP hit memory_limit. In Laravel it is usually a query loading too many models, a missing eager load or a growing queue worker. How to find and fix it. - [NET::ERR_CERT_DATE_INVALID: causes and fixes](/errors/err-cert-date-invalid/): Chrome shows NET::ERR_CERT_DATE_INVALID when the certificate expired or the visitor's clock is wrong. How to tell which, renew it and get warned in time. - [MySQL server has gone away (error 2006) in Laravel](/errors/mysql-server-has-gone-away/): MySQL closed the connection: an idle queue worker passed wait_timeout, a query hit max_allowed_packet or the server restarted. Fixes for Laravel. - [No application encryption key has been specified](/errors/no-application-encryption-key/): Laravel throws this when APP_KEY is empty in the config it loaded: no .env, a config cache built without the key, or a container that never got it. - [SQLSTATE[HY000] [2002] Connection refused in Laravel](/errors/sqlstate-hy000-2002/): Laravel can't reach MySQL. "Connection refused" is a wrong host or port or a stopped server; "No such file or directory" is DB_HOST=localhost using a socket. - [SQLSTATE errors in Laravel, code by code](/errors/sqlstate/): What SQLSTATE HY000 2002, 1045, 42000, 23000, 42S02 and 42S22 mean in a Laravel QueryException, how to tell which cause you have, and the fix for each. --- Source: https://lampwick.dev/errors/419-page-expired/ # 419 Page Expired and CSRF token mismatch in Laravel > Laravel returns 419 Page Expired, or "CSRF token mismatch." as JSON, when a POST has no valid CSRF token: no @csrf, an expired session or a cookie issue. `419 Page Expired` is the page Laravel shows when a `POST`, `PUT`, `PATCH` or `DELETE` request fails its CSRF check. An AJAX or API call that expects JSON gets the same failure as `{"message": "CSRF token mismatch."}` with status 419. The token in the request is missing, or it doesn't match the token in the user's session. The CSRF middleware throws `Illuminate\Session\TokenMismatchException`, and Laravel's exception handler turns it into an `HttpException` with status 419. 419 is not a standard HTTP status; Laravel uses it for this one case. Everything below was run on Laravel 13.40 and applies to Laravel 12 unless the text says otherwise. ## Laravel 12 and 13 check CSRF differently On Laravel 12, every non-read request through the `web` middleware group needs a token: the `_token` form field, an `X-CSRF-TOKEN` header or an `X-XSRF-TOKEN` header. Laravel 13 renamed the middleware to `PreventRequestForgery` and added a first check: when the browser sends `Sec-Fetch-Site: same-origin`, the request passes without a token ([Laravel 13 CSRF docs](https://laravel.com/docs/13.x/csrf#origin-verification)). Browsers send that header only over HTTPS (and to `localhost`). We confirmed it with `curl` against a route without a token: ```bash curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:8000/submit # 419 curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Sec-Fetch-Site: same-origin" http://127.0.0.1:8000/submit # 200 curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Sec-Fetch-Site: cross-site" http://127.0.0.1:8000/submit # 419 ``` So on Laravel 13 over HTTPS, most 419s from your own forms disappear. The ones that remain come from plain-HTTP sites, older browsers, other origins (a subdomain, a payment provider, an app on another port) and clients that aren't browsers, such as Postman, `curl` or a mobile app. On Laravel 12, every cause below applies to every request. ## The form has no `@csrf` The most common cause on a new form. Every HTML form that posts to a `web` route needs the hidden token field: ```blade
@csrf @method('PUT')
``` Confirm it: view the page source and look for ` ``` ```js await fetch('/cart/items', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify({ product_id: 42 }), }); ``` Axios needs nothing: it reads the encrypted `XSRF-TOKEN` cookie that Laravel sets and sends it as `X-XSRF-TOKEN` on same-origin requests. Livewire and Inertia also send the token for you; a 419 there means one of the session causes below. ## The session expired The CSRF token lives in the session. When the session is gone, so is the token, and the form that was rendered with it fails. This is the classic 419 after a user leaves a tab open over lunch: `SESSION_LIFETIME` is 120 minutes by default. Confirm it: load the form, delete the session cookie (named after your app, such as `laravel_session`) in the browser's dev tools, then submit. A 419 means this is your cause. Raise the lifetime if your users need it, and send them back to the form with their input instead of showing the bare 419 page. In `bootstrap/app.php`, catch the `HttpException` that the 419 becomes, because the handler converts the `TokenMismatchException` before your render callback sees it: ```php withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->render(function (HttpException $e, Request $request) { if ($e->getStatusCode() === 419 && ! $request->expectsJson()) { return back() ->withInput($request->except('_token', 'password', 'password_confirmation')) ->with('status', 'Your session expired. Please submit the form again.'); } }); })->create(); ``` The form now redirects back (302) with the old input and a message; JSON requests still get the 419. ## The session cookie never comes back If 419 happens on every submit, often right after login, the browser isn't sending the session cookie, so each request starts a new, empty session. Open dev tools → **Application** (or **Storage**) → **Cookies** and check whether the session cookie exists for the domain you're on. Then check `.env`: | Setting | What goes wrong | |---|---| | `SESSION_DOMAIN` | Set to a domain that isn't the one in the address bar (`.example.com` vs `app.example.org`), so the browser drops the cookie. Leave it empty unless you share sessions across subdomains. | | `SESSION_SECURE_COOKIE=true` | The cookie is only sent over HTTPS. On a plain-HTTP staging site or `http://localhost`, it never comes back. | | `SESSION_SAME_SITE=strict` or `none` | `strict` drops the cookie on posts that arrive from another site (an OAuth or payment redirect). `none` requires `SESSION_SECURE_COOKIE=true`, or browsers reject it. The default `lax` is right for most apps. | | `APP_URL` and the proxy | Behind a load balancer that terminates TLS, Laravel thinks the request is HTTP unless it trusts the proxy. Add `$middleware->trustProxies(at: '*');` in `bootstrap/app.php` (or the proxy's IP ranges). | ## Each server has its own sessions With two or more web servers behind a load balancer, the `file` session driver stores the session on whichever server rendered the form. The submit lands on the other server, which has no such session: 419, at random, about half the time. Use a shared store on every server: ```dotenv SESSION_DRIVER=database ``` Or `redis`. New Laravel apps default to `database` and ship the `sessions` table migration; older apps upgraded from Laravel 10 often still have `file`. All servers also need the same `APP_KEY`: session cookies are encrypted with it, and a server with another key can't read them. ## A deploy changed the key or the config A spike of 419s right after a deploy almost always means something in the deploy invalidated every open session: - `APP_KEY` changed. Every cookie encrypted with the old key is unreadable, so every user gets a new session and every open form fails. To rotate the key without this, put the old one in `APP_PREVIOUS_KEYS`; see [No application encryption key has been specified](/errors/no-application-encryption-key/#rotate-app-key). - The session driver or the Redis database changed, so existing sessions are gone. - `php artisan config:cache` ran with the wrong `.env` (a build container without your production values), so `SESSION_DOMAIN`, the driver or the key in the cached config differ from what you expect. Check with `php artisan config:show session`. ## A cache served someone else's token When a CDN or a full-page cache (Cloudflare "Cache Everything", Varnish, a response-cache package) caches a page with a form, every visitor gets the token of the user whose request filled the cache. None of them can submit it. Confirm it: compare the `_token` value in the page source in two private windows. If it's identical, the page is cached. Exclude pages with forms from the cache, or load the token with JavaScript after the page loads. ## The request isn't from a browser on your site Webhooks (Stripe, Mollie, GitHub), callbacks from a payment page and requests from Postman or a mobile app can't have a token from your session. Webhooks belong outside the `web` group, for example in `routes/api.php`. If a route must stay in `web`, exclude it in `bootstrap/app.php`: ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->validateCsrfTokens(except: [ 'webhooks/stripe', 'webhooks/*', ]); }) ``` `validateCsrfTokens` works on Laravel 12 and 13. Laravel 13 also has `preventRequestForgery(except: [...])`, which does the same. Excluding a route removes its CSRF protection entirely, so only exclude routes that verify the caller another way, such as a webhook signature. For Postman, either call the API routes with a token (Sanctum personal access tokens), or first `GET` a page, copy the `XSRF-TOKEN` cookie and send it back URL-decoded as the `X-XSRF-TOKEN` header. ## SPA with Sanctum: "CSRF token mismatch." on login A separate front end (Vue, React, Nuxt on another port or subdomain) that uses Sanctum's cookie authentication must ask for the CSRF cookie before it logs in, and send cookies with every request: ```js import axios from 'axios'; axios.defaults.withCredentials = true; axios.defaults.withXSRFToken = true; await axios.get('https://api.example.com/sanctum/csrf-cookie'); await axios.post('https://api.example.com/login', { email, password }); ``` On the Laravel side, check three things: - `$middleware->statefulApi();` is in `bootstrap/app.php`, so API routes get sessions. - `SANCTUM_STATEFUL_DOMAINS` lists the front end's host with its port, such as `app.example.com` or `localhost:5173`. - `SESSION_DOMAIN=.example.com` when the front end and API are on different subdomains, and `config/cors.php` has `'supports_credentials' => true`. Since axios 1.6, `withXSRFToken` must be `true` for the header to be sent to another origin; without it, every request fails with "CSRF token mismatch." even though the cookie is there. ## Catch 419s in production Laravel doesn't report a 419 as an exception: `TokenMismatchException` is on the handler's internal don't-report list, so it never reaches your log or an error tracker. You see 419s only in request data. In Lampwick, **Requests** lists each route with its 2xx/3xx, 4xx and 5xx counts and a chart over time; open a route such as `POST /login` to see each request with its status and user. A jump in 4xx on your form routes right after a time on **Deploys** points at the deploy causes above. The [Laravel package](/docs/laravel/) records every request without code changes. For other errors that do surface as exceptions, see the other [error pages](/errors/), starting with [SQLSTATE errors](/errors/sqlstate/). --- Source: https://lampwick.dev/errors/allowed-memory-size-exhausted/ # Allowed memory size of 134217728 bytes exhausted > PHP hit memory_limit. In Laravel it is usually a query loading too many models, a missing eager load or a growing queue worker. How to find and fix it. ```text PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) ``` The PHP process used all the memory `memory_limit` allows and was stopped. The first number is the limit in bytes; the second is the allocation that didn't fit, and it says nothing about the cause. Raising the limit makes the error go away until the data grows again, so first find what fills the memory. | Bytes in the message | `memory_limit` | |---|---| | 134217728 | `128M`, PHP's default | | 268435456 | `256M` | | 536870912 | `512M` | | 1073741824 | `1G` | ## Find which limit applies The CLI and PHP-FPM read different `php.ini` files, so a command can have a different limit from your web requests. Check both: ```bash php -i | grep -E "Loaded Configuration File|^memory_limit" php -r "echo ini_get('memory_limit'), PHP_EOL;" ``` For FPM, check the pool and its `php.ini` (on Debian and Ubuntu: `/etc/php/8.3/fpm/php.ini` and `/etc/php/8.3/fpm/pool.d/www.conf`, where `php_admin_value[memory_limit]` overrides it), or log `ini_get('memory_limit')` from a route. Restart PHP-FPM after a change. Composer is its own case: it raises its limit to 1.5 GB and can still run out on large dependency trees. Run it with `COMPOSER_MEMORY_LIMIT=-1 composer update`. ## A query loads every row into memory The most common cause in Laravel. `Order::all()` or `->get()` builds every model at once; 200,000 orders with their attributes can be gigabytes. It works in development with 50 rows and dies in production. Process large sets in pieces instead: ```php // One query per 1000 rows, one model in your loop at a time Order::where('status', 'paid')->lazy()->each(function (Order $order) { $order->sendReceipt(); }); // When you update the column you filter on, page by id so no rows are skipped Order::where('exported', false)->chunkById(500, function ($orders) { foreach ($orders as $order) { $order->update(['exported' => true]); } }); ``` `cursor()` runs one query and builds one model at a time, but MySQL's PDO driver still buffers the full result set in memory, and it can't eager load relations. `lazy()` is the safer default. For a count or a sum, let the database do it: `Order::where('status', 'paid')->sum('total')` instead of `->get()->sum('total')`. ## Relations load one by one, or all at once A loop that touches a relation on each model runs one query per model (the N+1 problem), and every loaded relation stays in memory with its parent. With `->with()` on a huge set, you load the parents and all their children in one go. ```php // Loads every order line of every order before the loop starts Order::with('lines')->get(); // Loads lines per chunk of 500 orders Order::with('lines')->chunkById(500, function ($orders) { foreach ($orders as $order) { // ... } }); ``` To find lazy loading in development, add `Model::preventLazyLoading(! app()->isProduction());` to `AppServiceProvider::boot()`. It throws on every relation that wasn't eager loaded. ## A queue worker grows until it dies A worker is one PHP process that handles job after job. Static caches, singletons that collect data, event listeners registered per job and big arrays in a service class survive between jobs, so memory creeps up over hours. `queue:work` checks its memory after each job and exits when it's above `--memory` (128 MB by default), so the process manager starts a fresh one: ```bash php artisan queue:work redis --memory=256 --max-jobs=500 --max-time=3600 ``` That only helps between jobs. A single job that needs more than PHP's `memory_limit` still fails with this error; fix the job with the chunking above, or split it into smaller jobs. Keep `--memory` well below `memory_limit`, so the worker restarts before PHP kills it. On Laravel 13, `--memory` also takes a percentage of `memory_limit`, such as `--memory=80%`. The same applies to Octane and to Artisan commands that run for hours: anything that keeps a reference to every model it has seen grows without limit. ## Exports, images and files Building an Excel export, a PDF or a ZIP in memory, or resizing a large image with GD, can take many times the file's size. Stream downloads with `response()->streamDownload()`, write exports in chunks (Laravel Excel's `FromQuery` with chunking instead of `FromCollection`), and resize images in a queued job with its own limit. ## Raise the limit where it belongs Once you know a job really needs more, raise the limit for that one place, not for the whole server: ```php public function handle(): void { ini_set('memory_limit', '512M'); // ... } ``` `memory_limit = -1` removes the limit, which turns a clean error into a server that swaps or an OOM-killed process. Don't use it in production. ## Catch it in production Laravel reports a memory error at shutdown as `Symfony\Component\ErrorHandler\Error\FatalError`; we confirmed it lands in the log with the same message. Laravel frees a small memory reserve at shutdown to report it, and the [Laravel package](/docs/laravel/) sends that error to Lampwick straight away, so it opens an issue in **Issues**, one per file and line where memory ran out, with the request or job it broke. Before it gets that far, each request and job timeline in Lampwick shows its **Peak memory**, so you can see which job class creeps toward the limit. A job that kills its worker this way isn't marked failed right away: it's handed out again after `retry_after` and ends with `MaxAttemptsExceededException`, as [why Laravel queued jobs fail](/guides/laravel-failed-jobs/) explains. For a connection that drops in a long-running worker, see [MySQL server has gone away](/errors/mysql-server-has-gone-away/). --- Source: https://lampwick.dev/errors/err-cert-date-invalid/ # NET::ERR_CERT_DATE_INVALID: causes and fixes > Chrome shows NET::ERR_CERT_DATE_INVALID when the certificate expired or the visitor's clock is wrong. How to tell which, renew it and get warned in time. `NET::ERR_CERT_DATE_INVALID` is Chrome's and Edge's error for a certificate that isn't valid on today's date: it expired, or it isn't valid yet. Firefox calls the same thing `SEC_ERROR_EXPIRED_CERTIFICATE`, Safari says the certificate "has expired". Either the site's certificate really is outside its dates, or the visitor's computer has the wrong date. ## Is it the site or the visitor? Check the certificate from another machine. If it's expired there too, the problem is the server: ```bash echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates ``` Replace `example.com` with the host. A healthy certificate: ```text subject=CN=lampwick.dev issuer=C=US, O=Let's Encrypt, CN=YE1 notBefore=Nov 7 13:17:13 2026 GMT notAfter=Feb 5 13:17:12 2027 GMT ``` An expired one, tested against `expired.badssl.com`: ```text notBefore=Apr 9 00:00:00 2015 GMT notAfter=Apr 12 23:59:59 2015 GMT ``` `curl https://example.com/` says `curl: (60) SSL certificate problem: certificate has expired` for the same case. If `notAfter` is in the future and only one visitor sees the error, it's on their side. ## Fixes on the visitor's side - **The device clock is wrong.** The most common visitor-side cause: a PC with a dead CMOS battery, a phone set to manual time, a virtual machine restored from a snapshot. Turn on automatic date and time and reload. - **Antivirus or a company proxy intercepts HTTPS.** It re-signs every site with its own certificate; when its own clock or certificate is off, every site fails. Test on another network. - **A very old device.** It doesn't know newer root certificates and may report an expired chain. Updating the OS or browser is the only real fix. If you run the site, none of these are yours to fix, but check the server first: a visitor reporting this is often the first sign that renewal failed. ## Fixes on the server: the certificate expired Certificates expire on purpose, and they're getting shorter. The CA/Browser Forum voted in April 2025 ([ballot SC-081v3](https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/)) to cut the maximum lifetime of public TLS certificates from 398 days to 47 days between March 2026 and March 2029: 200 days from 15 March 2026, 100 days from 15 March 2027, 47 days from 15 March 2029 ([DNSimple's summary](https://support.dnsimple.com/articles/announcement-ssl-certificate-validity-changes/)). Let's Encrypt issues 90-day certificates today and moves its default to 64 days on 10 February 2027 and 45 days on 16 February 2028 ([Let's Encrypt](https://letsencrypt.org/2025/12/02/from-90-to-45/)). All checked on 10 Nov 2026. Renewing by hand once a year no longer works: renewal has to be automatic, and you need to know when the automation breaks. Renew now with certbot, then find out why it didn't renew on its own: ```bash sudo certbot certificates sudo certbot renew --dry-run sudo certbot renew ``` `certbot certificates` lists each certificate with its expiry date. `--dry-run` tests renewal against Let's Encrypt's staging server without changing anything, and shows the error if there is one. ### Renewal isn't scheduled certbot installs a systemd timer or a cron job, and a server rebuilt from a script or a container without it never renews. Check: ```bash systemctl list-timers | grep certbot ``` No line means no schedule. With the snap package, `sudo systemctl enable --now snap.certbot.renew.timer`; otherwise add a cron entry that runs `certbot renew` twice a day. ### The challenge fails Let's Encrypt has to reach your server to prove you control the domain. `--dry-run` shows which part fails: - **Port 80 is closed.** HTTP-01 validation always starts on port 80, even when the site only serves HTTPS. Open it in the firewall and security group. - **A redirect or a new deploy broke `/.well-known/acme-challenge/`.** A changed web root, a catch-all route in your app, or a CDN that doesn't pass the path through. With certbot's `--webroot` mode, the web root must still be the directory the server serves. - **DNS moved.** The domain now points at a new server or a CDN, so the challenge never reaches the server that runs certbot. Renew where the traffic goes, or use DNS-01 validation. ### The certificate renewed, but the server still serves the old one Nginx, Apache and HAProxy read the certificate at start and keep it in memory. If nothing reloads them after renewal, the new file sits on disk while visitors get the expired one. Add a deploy hook, which certbot runs after every successful renewal: ```bash printf '#!/bin/sh\nsystemctl reload nginx\n' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx ``` Then check what the server actually serves with the `openssl s_client` command above, not the file on disk. ### Another server or the CDN has its own certificate Behind a load balancer or CDN, the visitor sees the certificate of whatever terminates TLS. One node with an old certificate gives the error to some visitors only. An origin certificate behind Cloudflare that expired gives Cloudflare error 526 instead. Check each node by its own IP: ```bash echo | openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate ``` `203.0.113.10` is the node's IP; `-servername` still sends the real host name. ### Hosted platforms On Vercel, Netlify, Lovable, Laravel Cloud, Forge and similar, the platform renews for you, as long as the domain's DNS still points at it. Renewal usually breaks when DNS was changed or a CAA record doesn't allow the platform's certificate authority. Check the domain's status in the platform's dashboard. ## Get warned before it expires Since 4 June 2025 Let's Encrypt no longer emails you before a certificate expires ([Let's Encrypt](https://letsencrypt.org/2025/01/22/ending-expiration-emails/), checked 10 Nov 2026). If renewal breaks, the first warning is often a visitor's screenshot of this error. Every Lampwick [uptime monitor](/docs/uptime/) also checks its URL's certificate from outside, every 6 to 12 hours. It alerts the team 14, 7, 3 and 1 days before expiry, and once when the certificate becomes invalid: expired, for the wrong host or from an unknown issuer. Because it reads the certificate the server actually serves, it also catches the "renewed but not reloaded" case. The details are in [SSL, domain and DNS checks](/docs/host-checks/#ssl-certificate). For the other reasons a site goes offline, such as an expired domain or a DNS change, see [how to know when your app is down](/guides/know-when-your-app-is-down/). --- Source: https://lampwick.dev/errors/mysql-server-has-gone-away/ # MySQL server has gone away (error 2006) in Laravel > MySQL closed the connection: an idle queue worker passed wait_timeout, a query hit max_allowed_packet or the server restarted. Fixes for Laravel. ```text SQLSTATE[HY000]: General error: 2006 MySQL server has gone away ``` The connection your PHP process had open was closed by the other side. The server timed it out, refused a packet that was too big, restarted, or something in between (a proxy, a load balancer) dropped it. The close of a connection mid-query shows up as `2013 Lost connection to MySQL server during query` instead; the causes are the same. ## Why Laravel usually hides it, and when it doesn't Laravel already reconnects for you. When a query fails with a lost-connection error, `Illuminate\Database\Connection` reconnects once and runs the query again. You see "gone away" only when that retry isn't allowed or doesn't help: - **Inside a transaction.** Laravel never retries a query inside `DB::transaction()` or after `DB::beginTransaction()`, because the earlier statements of that transaction are gone with the connection. The exception reaches your code. - **The retry fails the same way.** A packet that's too large is too large on the new connection too. - **The server is really gone**, so the reconnect fails with [SQLSTATE[HY000] [2002]](/errors/sqlstate-hy000-2002/). So the question is what kills your connections, and whether you hold a transaction open while it happens. ## A long-running process sat idle past `wait_timeout` The classic cause. A web request opens a connection and closes it within a second. A queue worker, `php artisan schedule:work`, an Octane worker or a long Artisan command keeps one connection open for hours. MySQL closes connections that have been idle longer than `wait_timeout`: ```sql SHOW VARIABLES WHERE Variable_name IN ('wait_timeout', 'interactive_timeout', 'max_allowed_packet'); ``` The MySQL default is 28800 seconds (8 hours), but managed and shared hosts often set 60 to 600 seconds. A worker on a quiet queue, or a job that spends 10 minutes on an API call between two queries, crosses that easily. The dangerous pattern is a transaction around slow work: ```php // Breaks when the API call takes longer than wait_timeout DB::transaction(function () use ($order) { $order->update(['status' => 'exporting']); $file = Http::timeout(900)->get($this->exportUrl)->body(); // minutes of idle connection $order->update(['status' => 'exported', 'file' => $file]); }); ``` Do the slow part outside the transaction, and keep transactions to the queries that must succeed together: ```php $order->update(['status' => 'exporting']); $file = Http::timeout(900)->get($this->exportUrl)->body(); DB::transaction(function () use ($order, $file) { $order->update(['status' => 'exported', 'file' => $file]); $order->events()->create(['type' => 'exported']); }); ``` If you can't avoid a long idle gap before a transaction, reconnect explicitly before you start it: ```php use Illuminate\Support\Facades\DB; DB::reconnect(); DB::transaction(function () { // ... }); ``` For workers, also make the processes themselves short-lived. They restart cleanly and pick up new code and config: ```bash php artisan queue:work redis --max-time=3600 --max-jobs=1000 ``` Run them under Supervisor or Forge's daemons so they come back after each exit. When a worker does hit a lost connection, Laravel stops it on purpose, so a worker without a process manager stays down. Raising `wait_timeout` on the server works too, but only if you control it, and it treats the symptom. ## A query or row is larger than `max_allowed_packet` Inserting a large blob, a big JSON column or a bulk insert of thousands of rows in one statement can exceed `max_allowed_packet`. MySQL then drops the connection, and PHP reports it as gone away. Because the retry sends the same packet, it fails every time, for the same data. Confirm it: the error comes from the same query every time, with large input. Fix it by inserting in smaller batches: ```php foreach (array_chunk($rows, 500) as $chunk) { DB::table('order_lines')->insert($chunk); } ``` Or raise the limit in the server config (`max_allowed_packet = 64M` under `[mysqld]`, which is the MySQL 8 default; MariaDB defaults to 16M) and restart MySQL. Large files belong in storage, with the path in the database. ## The server restarted, failed over or ran out of memory A MySQL restart, a managed database failover or maintenance window, or the Linux OOM killer stopping `mysqld` closes every connection at once. You see a burst of "gone away" across workers and requests at the same minute. Check the MySQL error log and `dmesg` for `Out of memory: Killed process … (mysqld)`, and the provider's event log for maintenance. ## A proxy or load balancer drops idle connections ProxySQL, RDS Proxy and cloud load balancers have their own idle timeouts, often shorter than MySQL's. Azure's load balancer, for example, drops idle TCP connections after 4 minutes by default ([Azure docs](https://learn.microsoft.com/azure/load-balancer/load-balancer-tcp-idle-timeout), checked 10 Nov 2026). The fixes are the same as for `wait_timeout`: shorter-lived workers, no idle transactions, and an explicit `DB::reconnect()` after a long pause. ## Catch it in production "Gone away" in a queue job usually means the job fails or is released, and it often happens at night on a quiet queue. With the [Laravel package](/docs/laravel/), Lampwick shows each job class under **Jobs** with its processed, released and failed counts, and every `QueryException` becomes an issue in **Issues** with the job it broke. A burst of the same issue across many jobs at one minute points at the server; the same job failing every time points at `max_allowed_packet` or a transaction around slow work. [Why Laravel queued jobs fail](/guides/laravel-failed-jobs/) covers what happens to the job after that, and the [SQLSTATE overview](/errors/sqlstate/) covers the other database errors. --- Source: https://lampwick.dev/errors/no-application-encryption-key/ # No application encryption key has been specified > Laravel throws this when APP_KEY is empty in the config it loaded: no .env, a config cache built without the key, or a container that never got it. `Illuminate\Encryption\MissingAppKeyException: No application encryption key has been specified.` means `config('app.key')` is empty at the moment Laravel needs the encrypter. Every web request needs it (cookies and sessions are encrypted), so the whole site returns 500. The value comes from `APP_KEY` in `.env` or the environment, unless a cached config overrides both. This is what the log says on Laravel 13.40 with `APP_KEY=` in `.env`: ```text [2026-11-10 16:38:59] production.ERROR: No application encryption key has been specified. {"exception":"[object] (Illuminate\\Encryption\\MissingAppKeyException(code: 0): No application encryption key has been specified. at .../vendor/laravel/framework/src/Illuminate/Encryption/EncryptionServiceProvider.php:83) ``` ## The key was never generated A fresh clone has `.env.example` but no `.env`, or a `.env` with an empty `APP_KEY=`. Generate one: ```bash cp .env.example .env php artisan key:generate ``` `key:generate` writes `APP_KEY=base64:…` into `.env`. In production it asks for confirmation; add `--force` in scripts. Without a `.env` file it fails with `file_get_contents(.../.env): Failed to open stream`, so in a container where the environment comes from elsewhere, generate the key once and store it as a secret instead: ```bash php artisan key:generate --show ``` `--show` prints a new key without writing it anywhere. Put that value in your host's secret store or environment settings (Forge, Laravel Cloud, Kubernetes secrets, the Docker Compose `environment:` block), not in the image. Generate the key once per environment and keep it. A new key on every deploy logs every user out and makes encrypted data unreadable. ## The config cache was built without the key `php artisan config:cache` writes `bootstrap/cache/config.php` with every value resolved at that moment. From then on Laravel reads only that file and ignores `.env`. Two ways this goes wrong: - The cache was built in a CI or Docker build step where `APP_KEY` wasn't set yet. The empty key is now baked into the image. - You added the key to `.env` on the server, but an old cache is still there. Confirm it: ```bash ls bootstrap/cache/config.php php artisan tinker --execute="var_dump(filled(config('app.key')));" ``` `bool(false)` with a cache file present means the cache holds an empty key. Rebuild it on the server, after the environment is in place: ```bash php artisan config:clear php artisan config:cache ``` Then restart PHP-FPM, Octane, queue workers and anything else that keeps the old config in memory (`php artisan queue:restart` for workers). In a container, run `config:cache` in the entrypoint at start, not in the `Dockerfile`. ## The variable doesn't reach PHP The key is set somewhere, but not where the PHP process looks: - **PHP-FPM clears the environment by default.** With `clear_env = yes` in the pool config, variables set for the container or the systemd unit never reach your app. Use a `.env` file, or set `clear_env = no` or `env[APP_KEY] = $APP_KEY` in the pool. - **The wrong `.env` file.** When `APP_ENV=staging` is set in the real environment (or you pass `--env=staging`), Laravel loads `.env.staging` if it exists, not `.env`. - **A deploy directory without it.** Zero-downtime deploys (Forge, Envoyer, Deployer) create a new release directory and symlink a shared `.env` into it. A broken symlink means no `.env`. Check what PHP actually sees, from the same user and directory as the web server: ```bash php artisan about --only=environment ``` ## The key exists but has the wrong format A key without the `base64:` prefix, or one that was cut off when it was copied, fails differently: ```text Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm. ``` The default cipher `AES-256-CBC` needs 32 bytes; `key:generate` produces `base64:` plus 44 characters. Copy the whole value, including `base64:`. ## Rotate APP_KEY without logging everyone out {#rotate-app-key} Changing `APP_KEY` breaks every session cookie and everything encrypted with `Crypt` or the `encrypted` cast. Users get logged out, open forms fail with [419 Page Expired](/errors/419-page-expired/), and decrypting old data throws `DecryptException`. Since Laravel 11, list the old key in `APP_PREVIOUS_KEYS` while you switch: ```dotenv APP_KEY="base64:NEW_KEY" APP_PREVIOUS_KEYS="base64:OLD_KEY" ``` `NEW_KEY` is the output of `php artisan key:generate --show`; `OLD_KEY` is the value you're replacing. Separate several old keys with commas. Laravel encrypts with the new key and tries the previous keys when decrypting. Re-encrypt stored data with the new key, then remove the old one. Every server needs the same `APP_KEY` and `APP_PREVIOUS_KEYS` at the same time. ## Catch it in production A missing key fails every page that uses sessions or cookies, so you notice it from outside first. Laravel's `/up` health route doesn't: it runs without the `web` middleware, and in our test it kept answering `200` while the home page returned `500`. Point a Lampwick [uptime monitor](/docs/uptime/) at a real page, such as your home page or `/login`. It alerts the team after two failed checks in a row, which with a 30-second interval is about a minute after the deploy. If your app sends exceptions with the [Laravel package](/docs/laravel/), the `MissingAppKeyException` also opens an issue under **Issues**, linked to the deploy that introduced it. Other errors that tend to appear right after a deploy: [SQLSTATE[HY000] [2002] Connection refused](/errors/sqlstate-hy000-2002/) and [419 Page Expired](/errors/419-page-expired/). --- Source: https://lampwick.dev/errors/sqlstate-hy000-2002/ # SQLSTATE[HY000] [2002] Connection refused in Laravel > Laravel can't reach MySQL. "Connection refused" is a wrong host or port or a stopped server; "No such file or directory" is DB_HOST=localhost using a socket. `SQLSTATE[HY000] [2002]` means PHP couldn't open a connection to the MySQL or MariaDB server at all: no query ran. The text after `[2002]` tells you why, and each variant has a different fix. Recent Laravel versions add the connection details to the message, which makes the cause easier to spot: ```text Illuminate\Database\QueryException: SQLSTATE[HY000] [2002] Connection refused (Connection: mysql, Host: 127.0.0.1, Port: 3306, Database: laravel, SQL: select 1) Illuminate\Database\QueryException: SQLSTATE[HY000] [2002] No such file or directory (Connection: mysql, Host: localhost, Port: 3306, Database: laravel, SQL: select 1) Illuminate\Database\QueryException: SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: nodename nor servname provided, or not known (Connection: mysql, Host: mysql, Port: 3306, Database: laravel, SQL: select 1) ``` These three came from one Laravel 13.40 app with no MySQL running, changing only `DB_HOST`. On Linux the third reads `Name or service not known` or `Temporary failure in name resolution`. ## "No such file or directory": localhost means a socket With `DB_HOST=localhost`, PHP's MySQL driver doesn't use TCP: it connects through a Unix socket file. "No such file or directory" means that file isn't where PHP looks. PHP looks at `pdo_mysql.default_socket`: ```bash php -i | grep pdo_mysql.default_socket # pdo_mysql.default_socket => /tmp/mysql.sock => /tmp/mysql.sock mysql -u root -p -e "SELECT @@socket;" ``` If the two paths differ, or MySQL isn't running so there is no socket, you get this error. Two fixes: ```dotenv # Use TCP instead of the socket DB_HOST=127.0.0.1 DB_PORT=3306 ``` ```dotenv # Or point Laravel at the real socket DB_HOST=localhost DB_SOCKET=/var/run/mysqld/mysqld.sock ``` `127.0.0.1` is the usual fix. It also means MySQL must listen on TCP: check `bind-address` and `skip-networking` in its config. ## "Connection refused": nothing listens on that host and port The host answered, but no database server listens on the port. In order of likelihood: 1. **MySQL isn't running.** `sudo systemctl status mysql` (or `mariadb`), `brew services list` on macOS. Start it, and check its error log if it won't start. 2. **Wrong port.** Another local install, or a Docker container mapped to `3307`. Check with `ss -ltnp | grep 3306` on Linux or `lsof -iTCP -sTCP:LISTEN | grep mysql` on macOS. 3. **MySQL listens on another interface.** `bind-address = 127.0.0.1` refuses connections from another server or container. On a separate database server, bind it to the private interface and allow your app server in the firewall. 4. **A managed database refuses your IP.** On RDS, DigitalOcean, PlanetScale and similar, the app server's IP or security group must be allowed. Test the connection without Laravel, from the app server: ```bash mysql -h 127.0.0.1 -P 3306 -u DB_USERNAME -p ``` `DB_USERNAME` is the user from your `.env`. If this fails too, the problem is the database or the network, not Laravel. ## In Docker, localhost is the container itself Inside a container, `127.0.0.1` and `localhost` point at that container, not at the MySQL container next to it. That gives "Connection refused" (or "No such file or directory" for `localhost`). Use the Compose service name as the host: ```yaml # compose.yaml services: app: build: . environment: DB_HOST: mysql DB_PORT: 3306 depends_on: mysql: condition: service_healthy mysql: image: mysql:8.4 environment: MYSQL_DATABASE: laravel MYSQL_ROOT_PASSWORD: secret healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] interval: 5s retries: 10 ``` The `healthcheck` with `condition: service_healthy` matters: MySQL takes several seconds to accept connections after its container starts, and an app that runs `php artisan migrate` on boot fails with "Connection refused" in that window. ## Laravel Sail: the host is `mysql` inside, `127.0.0.1` outside Sail's `.env` has `DB_HOST=mysql`, the service name. That works for everything run through Sail, because it runs inside the container network. Run the same command on your machine and the name doesn't resolve: that's the `getaddrinfo for mysql failed` variant. ```bash # Works: runs inside the container ./vendor/bin/sail artisan migrate # Fails with getaddrinfo for mysql: runs on your machine php artisan migrate ``` For a GUI client or a host-side command, connect to `127.0.0.1` with `FORWARD_DB_PORT` (3306 by default) instead. ## You changed .env, but Laravel still uses the old host If the message shows a `Host:` you already changed, the config is cached. `php artisan config:cache` froze the old values in `bootstrap/cache/config.php`: ```bash php artisan config:clear php artisan config:cache php artisan queue:restart ``` Restart PHP-FPM or Octane too. Long-running processes such as queue workers keep the old config until they restart. ## PostgreSQL gives a different message The same problem on PostgreSQL reads `SQLSTATE[08006] [7] connection to server at "127.0.0.1", port 5432 failed: Connection refused`. The causes are the same: the server isn't running, the host or port is wrong, or `listen_addresses` and `pg_hba.conf` don't allow your app. ## Catch it in production A database that can't be reached usually takes the whole app down, but Laravel's `/up` health route doesn't touch the database, so it keeps answering `200`. Make it check the connection: ```php // app/Providers/AppServiceProvider.php use Illuminate\Foundation\Events\DiagnosingHealth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen(DiagnosingHealth::class, function () { DB::select('select 1'); }); } ``` `/up` now returns `500` when the query throws. A Lampwick [uptime monitor](/docs/uptime/) on `/up` alerts the team after two failed checks in a row. With the [Laravel package](/docs/laravel/), each `QueryException` also becomes one issue in **Issues** with its count and the requests and jobs it broke; see [the other SQLSTATE codes](/errors/sqlstate/) for the errors that come after the connection works. --- Source: https://lampwick.dev/errors/sqlstate/ # SQLSTATE errors in Laravel, code by code > What SQLSTATE HY000 2002, 1045, 42000, 23000, 42S02 and 42S22 mean in a Laravel QueryException, how to tell which cause you have, and the fix for each. A Laravel `QueryException` starts with the database's error code: `SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry …`. The five characters in brackets are the standard SQLSTATE class; the number after them is MySQL's or MariaDB's own error code, and that is the one to search for. This page covers the codes Laravel developers hit most, in the order a new app tends to meet them. | Message starts with | Means | Section | |---|---|---| | `SQLSTATE[HY000] [2002]` | Can't connect to the server at all | [2002](#hy000-2002) | | `SQLSTATE[HY000] [1045]` | Connected, but the login was refused | [1045](#hy000-1045) | | `SQLSTATE[42S02]` | The table doesn't exist | [42S02](#42s02) | | `SQLSTATE[42S22]` | A column doesn't exist | [42S22](#42s22) | | `SQLSTATE[42000]` | SQL syntax error, or a limit or mode the query breaks | [42000](#42000) | | `SQLSTATE[23000]` | A constraint refused the row: duplicate, foreign key or NULL | [23000](#23000) | ## SQLSTATE[HY000] [2002] Connection refused {#hy000-2002} PHP couldn't reach the database server: wrong host or port, the server isn't running, or `DB_HOST=localhost` makes PHP look for a socket file that isn't there ("No such file or directory"). In Docker and Sail, the host is the service name, not `127.0.0.1`. This one has its own page: [SQLSTATE[HY000] [2002] Connection refused](/errors/sqlstate-hy000-2002/). ## SQLSTATE[HY000] [1045] Access denied for user {#hy000-1045} ```text SQLSTATE[HY000] [1045] Access denied for user 'forge'@'10.0.1.12' (using password: YES) ``` The server is reachable and rejected the username, password or the host you connect from. `using password: NO` means Laravel sent an empty password, so `DB_PASSWORD` didn't load. A common cause is a password with a `#` in `.env`. Without quotes, everything from `#` on is a comment. We checked it on Laravel 13.40: ```dotenv DB_PASSWORD=ab#cd # Laravel reads "ab" DB_PASSWORD="ab#cd" # Laravel reads "ab#cd" ``` Double quotes also expand `${OTHER_VAR}`; use single quotes for a password that contains `${`. Other causes: - The MySQL user exists only for `'user'@'localhost'`, and you now connect over TCP from another host or container. Create it for the right host: `CREATE USER 'forge'@'10.0.1.%' IDENTIFIED BY '…';` and grant it the database. - The config is cached with the old password: `php artisan config:clear && php artisan config:cache`. - On Ubuntu, MySQL's `root` uses socket authentication and has no password. Create a separate user for the app. ## SQLSTATE[42S02]: Base table or view not found: 1146 {#42s02} ```text SQLSTATE[42S02]: Base table or view not found: 1146 Table 'shop.order_product' doesn't exist ``` The query names a table that isn't in the database it's connected to. - **Migrations didn't run** on this environment. `php artisan migrate:status` shows which are pending; deploy scripts need `php artisan migrate --force`. - **Wrong database.** The message names it (`shop.`); compare it with `DB_DATABASE`. - **Laravel guessed the name.** A model `OrderItem` uses `order_items`; a `belongsToMany` between `Order` and `Product` uses `order_product`, in alphabetical order. Set `protected $table = 'order_lines';` on the model, or pass the pivot name: `$this->belongsToMany(Product::class, 'product_orders')`. - **Sessions, cache or queue on the database driver** without their tables. New apps need the `sessions`, `cache` and `jobs` tables from the default migrations. ## SQLSTATE[42S22]: Column not found: 1054 {#42s22} ```text SQLSTATE[42S22]: Column not found: 1054 Unknown column 'discount_total' in 'field list' ``` A query names a column the table doesn't have. - The migration that adds it hasn't run here (`migrate:status`), or ran on another database. - A typo, or a column that was renamed in a migration while the code still uses the old name. - `Unknown column 'updated_at'`: the table has no timestamps. Add `public $timestamps = false;` to the model, or add `$table->timestamps()` to the table. - An accessor or appended attribute that you write back with `$model->update($request->all())`. Validate the input and pass only real columns. ## SQLSTATE[42000]: Syntax error or access violation {#42000} 42000 groups several different MySQL errors. The number after it tells them apart: | Code | Message | Cause and fix | |---|---|---| | 1064 | `You have an error in your SQL syntax` | Usually a raw query: a reserved word as a column name (`order`, `group`, `key`) without backticks, or string concatenation instead of bindings. Use the query builder or `DB::select('… where id = ?', [$id])`. | | 1055 | `… isn't in GROUP BY` | Laravel runs MySQL in strict mode, which includes `ONLY_FULL_GROUP_BY`. Select only grouped columns and aggregates, or wrap the others in `ANY_VALUE()`. Turning off `'strict'` in `config/database.php` hides the problem instead of fixing it. | | 1071 | `Specified key was too long; max key length is 767 bytes` | MySQL before 5.7.7 or MariaDB before 10.2.2 with `utf8mb4`. Upgrade the server, or add `Schema::defaultStringLength(191);` to `AppServiceProvider::boot()`. | | 1142 / 1044 | `… command denied to user` / `Access denied for user … to database` | The database user lacks a privilege, often `CREATE` or `ALTER` during migrations, or access to the database at all. Grant it. | ## SQLSTATE[23000]: Integrity constraint violation {#23000} ```text SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'ord_8F2K' for key 'orders.number' ``` A constraint refused the row. Again, the number decides the fix: - **1062 Duplicate entry.** A unique index already has this value. If users submit it, validate first with `Rule::unique('orders', 'number')`. If two requests or jobs race, validation can't help: let the index decide and handle the result. Laravel throws `Illuminate\Database\UniqueConstraintViolationException` for this case: ```php use Illuminate\Database\UniqueConstraintViolationException; try { $order = Order::create(['number' => $number, 'user_id' => $user->id]); } catch (UniqueConstraintViolationException) { $order = Order::where('number', $number)->firstOrFail(); } ``` `Order::createOrFirst([...])` does the same in one call. Retried queue jobs are a common source: a job that inserts a row and then fails runs again and hits the duplicate. - **1452 Cannot add or update a child row: a foreign key constraint fails.** The referenced row doesn't exist, often because it was created in a transaction that hasn't committed yet, or in a queued job dispatched before the commit. Dispatch with `->afterCommit()`. - **1451 Cannot delete or update a parent row.** Other rows still reference this one. Delete them first, or use `->cascadeOnDelete()` on the foreign key. - **1048 Column 'x' cannot be null.** The value is missing; the column needs a default, `->nullable()`, or the code must set it. ## Catch SQLSTATE errors in production Every `QueryException` that isn't caught reaches Laravel's exception handler. With the [Laravel package](/docs/laravel/), Lampwick groups them into issues by class, code, file and line, so a thousand duplicate-entry errors from one `create()` call are one row in **Issues** with a count, the users affected and the request or job each one broke. See [work with issues](/docs/issues/) for resolving them and getting notified when one comes back. Related: [MySQL server has gone away](/errors/mysql-server-has-gone-away/), the error you see when a connection that worked is lost. --- Source: https://lampwick.dev/for/ # Lampwick for AI-built apps and agencies > For apps built with Lovable, Replit or v0, and agencies running client sites: paste a URL for uptime, SSL, domain and DNS checks, then connect your agent. Lampwick checks an app built with Lovable, Replit or v0 from the outside, with no code: paste its URL and you get uptime, SSL, domain and DNS checks, a site crawl for broken links and alerts. One line from your builder adds [browser errors](/docs/browser/). Then connect your builder's chat to Lampwick, so it can read what Lampwick found and propose the fix. If you run sites for clients, give each client a project. If you write the code yourself, let your coding agent [set Lampwick up](/docs/agents/), or connect the app with [OpenTelemetry](/docs/opentelemetry/), the [Laravel package](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/). - [Monitoring for agencies with many client sites](/for/agencies/): One Lampwick team, one project per client: uptime, SSL, domain and errors for every client app, alerts per client channel and client status pages. - [Monitor apps built with Lovable, Replit or v0](/for/ai-built-apps/): Paste your app's URL. Lampwick checks uptime, SSL, domain and DNS, finds broken links, gives you a status page and alerts your phone. No code. - [Lovable app monitoring: uptime, SSL and DNS](/for/lovable/): Add your Lovable app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect Lovable's chat to Lampwick over MCP. - [Replit app monitoring: uptime, SSL and DNS](/for/replit/): Add your published Replit app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect Replit Agent to Lampwick over MCP. - [v0 app monitoring: uptime, SSL and DNS](/for/v0/): Add your published v0 app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect v0 to Lampwick over MCP. --- Source: https://lampwick.dev/for/agencies/ # Monitoring for agencies with many client sites > One Lampwick team, one project per client: uptime, SSL, domain and errors for every client app, alerts per client channel and client status pages. Put each client in its own Lampwick project, inside one team. Every client site gets uptime, SSL, domain and DNS checks from its URL alone; the apps you build get errors, slow requests and failing jobs as well. Your whole team sees every client, each client's alerts go to that client's channel, and a client can have a public status page. ## Projects, checks, alerts and roles per client - **One project per client.** Monitors, issues, requests and logs stay apart per client. The project switcher and ⌘ K jump between them; **All projects** shows them together. - **Site checks from a URL.** For every site you look after, also the ones you didn't build: [uptime](/product/#uptime), the [SSL certificate, domain registration and DNS](/docs/host-checks/), and, from Pro, a daily [broken link and mixed content crawl](/product/#broken-links) for up to 10 sites per team. No access to the client's server needed. - **Inside the apps you build.** The [Laravel package](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/) adds [exceptions, requests, queries and jobs](/product/#issues), and [Schedule health](/product/#jobs) for the scheduler. - **Alerts per client.** Everyone on the team gets push and email. An [alert channel](/docs/alerts/#alert-channels) can take the alerts of one project, so client A's alerts land in `#client-a` on Slack, Discord or Microsoft Teams, or in your own ticketing system through a signed webhook. Up to 20 channels per team. - **Status pages for clients (Pro and up).** A public page per client at `app.lampwick.dev/status/client-name`, with its monitors, daily uptime and your incident notices ([status pages](/product/#status-pages)). - **Roles.** Owners manage ingest tokens, status pages, invites and deletes; members work on the projects: issues, monitors, scheduled tasks ([teams and roles](/docs/teams/)). Each person can turn on two-factor authentication. - **An agent across all clients.** Over the [MCP server](/docs/mcp/), ask "Which uptime monitors are down right now?" or "Which scheduled tasks are failing?" across every client project, read-only by default. ## Set it up 1. [Create an account](https://app.lampwick.dev/signup). The team you get is yours. 2. Create a project per client, and under **Uptime** add a monitor per site. 3. Invite your colleagues under **Teams** → **Invite people**, as **Member** or **Owner**. 4. Add an alert channel per client under **Alert channels**, with **Alerts of** set to that client's project. 5. For a client status page: **Status pages** → **Create status page**, tick the client's monitors, **Save and publish**. ## Which plan fits Prices are per team per month, excluding VAT. Seats are unlimited from Pro. | Plan | Price | Client projects | Uptime monitors | Events / month | |---|---|---|---|---| | Free | €0 | 1 | 3, every 5 min | 500k | | Pro | €29 | 5 | 20, every minute | 5M | | Team | €79 | 25 | 100, every 30 s | 25M | | Business | €199 | unlimited | 300, every 30 s | 100M | Events only count what your apps send (exceptions, requests, logs); monitors for sites you only check from outside use none. Status pages and the site crawl need Pro or higher. See [pricing](/pricing/). ## What doesn't fit yet - **Client access is per team, not per project.** A member sees every project of the team. To let a client log in and see only their own site, give them a separate team, which has its own plan and bill. - **Status pages: up to 10 per team**, on `app.lampwick.dev/status/…`, without a custom domain or your logo. - **Site crawl on up to 10 monitors per team**, 20 over all teams you own. - **No client reports.** There is no white-label or scheduled PDF uptime report to send to clients; the status page and the [API](/docs/api/) are what you can share. - **Checks from one location**, and no SMS or PagerDuty alerts. If your work is mostly site checks for many client sites, without app monitoring, read [Lampwick vs Oh Dear](/compare/lampwick-vs-oh-dear/): it says where Oh Dear is the better pick. [Start free](https://app.lampwick.dev/signup) with your first client. --- Source: https://lampwick.dev/for/ai-built-apps/ # Monitor apps built with Lovable, Replit or v0 > Paste your app's URL. Lampwick checks uptime, SSL, domain and DNS, finds broken links, gives you a status page and alerts your phone. No code. Paste your app's address into Lampwick. It checks that the app loads, that its certificate and domain don't expire and that its links work, and tells your phone when something breaks. Your app's code stays as it is. ## A check that doesn't run on your platform Lovable and Replit have their own monitoring on paid plans ([Lovable](https://docs.lovable.dev/features/project-monitoring), [Replit](https://docs.replit.com/references/publishing/monitoring-a-deployment), checked 8 Nov 2026). Keep it on, but it runs on the same platform as your app. Lampwick runs on its own servers, so a platform outage doesn't take the check down too. ## What Lampwick checks - **Uptime.** Lampwick opens your URL every 5 minutes on Free, more often on paid plans. Two failures in a row and the monitor is down. - **SSL certificate.** The certificate puts the padlock in the address bar. Alerts 14, 7, 3 and 1 days before it expires, and right away if browsers would reject it. - **Domain.** Alerts 30, 14, 7 and 1 days before your domain registration expires. For some endings, such as `.nl`, the date isn't public; Lampwick then shows *unknown*. - **DNS.** DNS is the address book that sends visitors to your app. When its main records change, you get *DNS records changed*. - **Site crawl** (Pro and up). Once a day Lampwick follows your links, up to 250 pages, and lists broken links and mixed content (an `http://` image or script on an `https://` page, which browsers block). It doesn't run JavaScript, so it finds few links on apps that draw them with JavaScript. - **Status page** (Pro and up). A public page at `app.lampwick.dev/status/your-name` that shows whether your app is up. - **Alerts.** A push from the Lampwick iPhone app and an email: *Down: My app*, then *Up again: My app*. Slack, Discord or Microsoft Teams work too ([alert channels](/docs/alerts/#alert-channels)). ## Set it up without code 1. Go to [app.lampwick.dev/signup](https://app.lampwick.dev/signup), enter your email and a password of at least 12 characters, and click **Create account**. 2. Open Lampwick's email, click the link, enter your password again and click **Confirm and log in**. 3. On **Welcome to Lampwick**, type a name such as *My app* and click **Create project**. 4. Skip the **Connect your app** card (it's for sending data from code) and click **Uptime** in the sidebar. 5. Under **Add monitor**, paste your app's address, such as `https://myapp.com`, and click **Add monitor**. It shows **Pending**, then **Up**. SSL, domain and DNS checks are on by default. 6. For broken links (Pro and up): click the monitor, then **Turn on site crawl**. 7. For a status page (Pro and up): click your project's name at the top of the sidebar, choose **All projects**, then click **Status pages**. Fill in **Title** and **Slug** (the end of the address), click **Create status page**, tick your monitor and click **Save and publish**. 8. For push alerts, install the Lampwick iPhone app and log in with the same account. Monitor your own domain if you have one (`myapp.com`, not `myapp.lovable.app`): its renewal is your job, not the platform's. ## Let your builder's agent read it MCP is a standard way for an AI agent to use another app's tools. Connect your builder to Lampwick, then ask its chat "Is my app up, and when does its certificate expire?" Steps for [Lovable](/for/lovable/), [Replit](/for/replit/) and [v0](/for/v0/); for Bolt it's not confirmed yet. ### A prompt for your builder Once your builder is connected to Lampwick, paste this into its chat: ```text Use the Lampwick connector. Tell me whether my uptime monitor is up and when my SSL certificate and domain expire. If Lampwick has errors from my app's visitors, find the cause in this project and fix it. ``` ## What it costs Per team, per month, excluding VAT. Billed yearly, a plan costs ten months: 2 months free. | Plan | Price | Projects | Uptime monitors | |---|---|---|---| | Free | €0 | 1 | 3, every 5 min | | Pro | €29 | 5 | 20, every minute | | Team | €79 | 25 | 100, every 30 s | | Business | €199 | unlimited | 300, every 30 s | SSL, domain and DNS checks come with every plan. The site crawl and status pages need Pro or higher. ## What we don't cover yet The checks above see whether your app loads, not what happens inside it. To catch a sign-up button that throws, add the [browser error snippet](/docs/browser/): your builder adds one line to the app from a prompt you copy out of Lampwick. Stack traces then point into the bundled code, because source maps aren't supported yet. Errors in your app's backend, such as a database function, reach Lampwick only if it sends them, which takes code ([HTTP ingest](/docs/http-ingest/)). To check the outside yourself first, read [how to know when your app is down](/guides/know-when-your-app-is-down/). [Start free](https://app.lampwick.dev/signup) and paste your URL. --- Source: https://lampwick.dev/for/lovable/ # Lovable app monitoring: uptime, SSL and DNS > Add your Lovable app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect Lovable's chat to Lampwick over MCP. Paste your published Lovable app's address into Lampwick and it checks the app from outside Lovable, with alerts on your phone. Connect Lovable's chat to Lampwick and it can read what Lampwick found and propose the fix. ## Where your Lovable app runs When you publish, Lovable hosts your app at an address ending in `lovable.app`. On a paid plan you can connect your own domain under **Project → Settings → Domains**, and Lovable sets up its SSL certificate ([Lovable docs: custom domain](https://docs.lovable.dev/features/custom-domain), checked 8 Nov 2026). Monitor the address your visitors use. On your own domain, its renewal and DNS are your job, and Lampwick watches both. Lovable's own **Project monitoring** (beta, Pro plans and up) reads your code and visitor errors once a day ([Lovable docs](https://docs.lovable.dev/features/project-monitoring), checked 8 Nov 2026). Lampwick checks from outside Lovable and doesn't read code. Use both. ## Monitor it without code 1. [Create a Lampwick account](https://app.lampwick.dev/signup), confirm your email, and create a project named after your app. 2. Click **Uptime** in the sidebar. Under **Add monitor**, paste your app's address, such as `https://myapp.com`, and click **Add monitor**. 3. The monitor shows **Pending**, then **Up**. SSL, domain and DNS checks are on already. Every click, plus the status page: [the overview](/for/ai-built-apps/#set-it-up-without-code). On Lovable apps created before 13 May 2026, Lampwick's site crawl finds few links: those apps build pages with JavaScript and send finished pages only to verified crawlers such as search engines ([Lovable docs: SEO](https://docs.lovable.dev/features/seo-aeo), checked 8 Nov 2026). Newer apps send them to everyone. ## Connect Lovable to Lampwick MCP is a standard way for an AI agent to use another app's tools; Lovable calls these **Connectors**. Connected, Lovable can look up your monitors, expiry dates and broken links. Add Lampwick in Lovable ([Lovable docs: custom MCP](https://docs.lovable.dev/integrations/custom-mcp), checked 9 Nov 2026): 1. Open **Connectors**, click **Add custom** at the top right, and choose **MCP server**. If it's grayed out, a workspace admin has turned custom MCP servers off. 2. **Server name**: `lampwick`. **Server URL**: `https://app.lampwick.dev/mcp`. 3. For authentication, keep **OAuth** and click **Add & authorize**. 4. Lampwick opens. Log in, choose **Read-only (recommended)** and click **Connect**. Read-only lets Lovable look at everything in Lampwick but change nothing. Disconnect it any time under **Account & security** → **Connected clients**. If Lovable can't connect this way, use a read-only token instead ([connect with a personal token](/docs/mcp/#token)). Lovable uses the connection only in your chat, never in your published app. ## Check that it works In your project's chat, ask "Which Lampwick projects can you see?". Lovable should name your project. If it isn't authorized, remove the server in **Connectors** and add it again. Then try: - "Is my Lampwick monitor up, and when does my SSL certificate expire?" - "Which broken links did Lampwick's last crawl find? Fix the ones in this project." (site crawl: Pro and up) Errors inside your app, such as a button that throws, reach Lampwick once you add the [browser error snippet](/docs/browser/). Paste the prompt from your project's **Settings** → **Browser script** into Lovable's chat, and it adds one line to your app. ## A prompt for Lovable Once Lovable is connected to Lampwick, paste this into Lovable's chat: ```text Use the Lampwick connector. Tell me whether my uptime monitor is up and when my SSL certificate and domain expire. If Lampwick has errors from my app's visitors, find the cause in this project and fix it. ``` [Start free](https://app.lampwick.dev/signup), or read the [MCP reference](/docs/mcp/). --- Source: https://lampwick.dev/for/replit/ # Replit app monitoring: uptime, SSL and DNS > Add your published Replit app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect Replit Agent to Lampwick over MCP. Paste your published Replit app's address into Lampwick and it checks the app from outside Replit, with alerts on your phone. Connect Replit Agent to Lampwick and it can read what Lampwick found and propose the fix. ## Where your Replit app runs When you click **Publish**, Replit runs your app at an address ending in `.replit.app`, separate from the preview you see while building ([Replit docs: publish your app](https://docs.replit.com/build/publish-your-app), checked 8 Nov 2026). You can also connect or [buy](https://docs.replit.com/cloud-services/deployments/domain-purchasing) your own domain. Monitor the published address, not the preview. On paid plans Replit emails you when a published app goes down ([Replit docs: monitoring](https://docs.replit.com/references/publishing/monitoring-a-deployment), checked 8 Nov 2026). Lampwick adds a check from outside Replit, certificate, domain and DNS checks, a status page and push alerts. ## Monitor it without code 1. [Create a Lampwick account](https://app.lampwick.dev/signup), confirm your email, and create a project named after your app. 2. Click **Uptime** in the sidebar. Under **Add monitor**, paste your app's address, such as `https://myapp.replit.app` or your own domain, and click **Add monitor**. 3. The monitor shows **Pending**, then **Up**. SSL, domain and DNS checks are on already. Every click, plus the status page: [the overview](/for/ai-built-apps/#set-it-up-without-code). If your app's access is set to **Password protected** or **Invite only**, Lampwick gets a login page instead of your app. ## Connect Replit Agent to Lampwick MCP is a standard way for an AI agent to use another app's tools. Connected, Replit Agent can look up your monitors, expiry dates and broken links, in all your Replit projects. Add Lampwick in Replit ([Replit docs: connect via MCP](https://docs.replit.com/build/connect-via-mcp), checked 9 Nov 2026): 1. Go to [replit.com/integrations](https://replit.com/integrations), click **Add custom**, then **Add personal MCP server**. 2. **Display name**: `lampwick`. 3. **MCP Server URL**: `https://app.lampwick.dev/mcp`. 4. Click **Test & save**. Replit opens Lampwick's sign-in: log in, choose **Read-only (recommended)** and click **Connect**. Lampwick appears under **MCP Servers**. Read-only lets Replit Agent look at everything in Lampwick but change nothing. Disconnect it any time under **Account & security** → **Connected clients**. If Replit Agent can't connect this way, use a read-only token instead ([connect with a personal token](/docs/mcp/#token)). ## Check that it works Ask Replit Agent "Which Lampwick projects can you see?". It should name your project. If **Test & save** shows an error, try again and finish the sign-in in the window Lampwick opens. Then try: - "Is my Lampwick monitor up, and when does my SSL certificate expire?" - "Which broken links did Lampwick's last crawl find? Fix the ones in this app." (site crawl: Pro and up) Errors inside your app, such as a button that throws, reach Lampwick once you add the [browser error snippet](/docs/browser/). Paste the prompt from your project's **Settings** → **Browser script** into Replit Agent's chat, and it adds one line to your app. ## A prompt for Replit Agent Once Replit Agent is connected to Lampwick, paste this into Replit Agent's chat: ```text Use the Lampwick connector. Tell me whether my uptime monitor is up and when my SSL certificate and domain expire. If Lampwick has errors from my app's visitors, find the cause in this project and fix it. ``` [Start free](https://app.lampwick.dev/signup), or read the [MCP reference](/docs/mcp/). --- Source: https://lampwick.dev/for/v0/ # v0 app monitoring: uptime, SSL and DNS > Add your published v0 app's URL to Lampwick for uptime, SSL, domain and DNS checks without code, and connect v0 to Lampwick over MCP. Paste your published v0 app's address into Lampwick and it checks the app from outside Vercel, with alerts on your phone. Connect v0 to Lampwick and it can read what Lampwick found and propose the fix. ## Where your v0 app runs When you click **Publish** in the chat header, v0 deploys your app to Vercel at an address ending in `vercel.app`. For your own domain, open **Publish → Customize Domain** and finish the DNS setup in the Vercel dashboard ([v0 docs: deployments](https://v0.app/docs/deployments), [custom domains](https://v0.app/docs/custom-domains), checked 8 Nov 2026). Monitor your production address, not a preview deployment. On your own domain, its renewal and DNS are your job, and Lampwick watches both. ## Monitor it without code 1. [Create a Lampwick account](https://app.lampwick.dev/signup), confirm your email, and create a project named after your app. 2. Click **Uptime** in the sidebar. Under **Add monitor**, paste your app's address, such as `https://myapp.com`, and click **Add monitor**. 3. The monitor shows **Pending**, then **Up**. SSL, domain and DNS checks are on already. Every click, plus the status page: [the overview](/for/ai-built-apps/#set-it-up-without-code). If you've turned on Vercel's protection for your deployment, Lampwick gets a login page instead of your app. ## Connect v0 to Lampwick MCP is a standard way for an AI agent to use another app's tools. Connected, v0 can look up your monitors, expiry dates and broken links while you chat. Add Lampwick in v0 ([v0 docs: MCP](https://v0.app/docs/MCP), checked 9 Nov 2026): 1. In the prompt box, open the **+** menu and choose **MCPs**. 2. Add a custom server, not a preset. Name it `lampwick`, with the URL `https://app.lampwick.dev/mcp`. 3. For authentication, choose **OAuth** and save. 4. Lampwick opens. Log in, choose **Read-only (recommended)** and click **Connect**. Read-only lets v0 look at everything in Lampwick but change nothing. Disconnect it any time under **Account & security** → **Connected clients**. If v0 can't connect this way, use a read-only token instead ([connect with a personal token](/docs/mcp/#token)). ## Check that it works Ask v0 "Which Lampwick projects can you see?". It should name your project. If it reports an authorization error, remove the server and add it again. Then try: - "Is my Lampwick monitor up, and when does my SSL certificate expire?" - "Which broken links did Lampwick's last crawl find? Fix the ones in this app." (site crawl: Pro and up) Errors inside your app, such as a button that throws, reach Lampwick once you add the [browser error snippet](/docs/browser/). Paste the prompt from your project's **Settings** → **Browser script** into v0's chat, and it adds one line to your app. ## A prompt for v0 Once v0 is connected to Lampwick, paste this into v0's chat: ```text Use the Lampwick connector. Tell me whether my uptime monitor is up and when my SSL certificate and domain expire. If Lampwick has errors from my app's visitors, find the cause in this project and fix it. ``` [Start free](https://app.lampwick.dev/signup), or read the [MCP reference](/docs/mcp/). --- Source: https://lampwick.dev/guides/ # Guides: failed jobs, the scheduler, logs and downtime > How to find out when a Laravel job fails, a scheduled task doesn't run or your app goes down, and how to debug production errors with Claude Code. How to find and fix things that fail without telling you: queued jobs, the Laravel scheduler, logs and downtime. Two more cover setting up Lampwick with Claude Code and debugging production errors with it. - [Claude Code for Laravel: debug production errors](/guides/debug-laravel-production-errors-with-claude-code/): Use Claude Code with Laravel in production: connect it to your error tracker over MCP, let it read the stack trace and code, and review its fix. - [How to check if your website is down](/guides/know-when-your-app-is-down/): Check if your site is down for everyone or just you, then find which of five causes it is: host, SSL, domain, DNS or broken links. And get told next time. - [Laravel cron job setup for the scheduler](/guides/laravel-cron-job-setup/): Laravel needs one crontab line running php artisan schedule:run every minute. How to add it on a server, Forge, Ploi or Docker, and why it may not run. - [Laravel queue monitoring: failed jobs, timeouts, retries](/guides/laravel-failed-jobs/): Laravel jobs fail by throwing, timing out or running out of attempts (MaxAttemptsExceededException). How retries work, and how to get alerted. - [Laravel logging in production: a setup guide](/guides/laravel-logging-in-production/): Laravel logs debug lines to one file by default. In production: daily files or stderr, LOG_LEVEL=info, JSON, shared context, and logs off the server. - [Monitor the Laravel scheduler, and why tasks silently don't run](/guides/monitor-laravel-scheduler/): Laravel's scheduler runs only when cron calls schedule:run each minute. Missing cron, stuck locks and timezones skip tasks silently; a heartbeat catches it. - [Set up monitoring with Claude Code](/guides/set-up-monitoring-with-claude-code/): Ask Claude Code to add error and uptime monitoring to your app: it installs the package, keeps the token in .env, sends a test event and checks it arrived. - [Telescope vs Pulse vs Nightwatch in production](/guides/telescope-pulse-in-production/): Telescope inspects single requests, Pulse aggregates, Nightwatch is hosted and alerts. How to run each in production, and yes, Nightwatch has a free plan. --- Source: https://lampwick.dev/guides/debug-laravel-production-errors-with-claude-code/ # Claude Code for Laravel: debug production errors > Use Claude Code with Laravel in production: connect it to your error tracker over MCP, let it read the stack trace and code, and review its fix. Claude Code can debug a Laravel app's production errors when your error tracker runs an MCP server: add the server once, then ask "what broke since this morning?" from your repository. It reads the stack trace, opens the files it points to and proposes a fix that you review like any other diff. This guide uses Lampwick's MCP server; the workflow and safety rules apply to any error tracker with one. ## What you need - [Claude Code](https://code.claude.com/docs/en/mcp) in the repository of the app that throws the errors. - An MCP server for your errors. For Lampwick: a personal API token from **Account & security** (it starts with `lwp_`) and the [Laravel package](/docs/laravel/) sending data. No monitoring yet? Claude Code can [set it up for you](/guides/set-up-monitoring-with-claude-code/). - A checkout of the commit that runs in production (Lampwick stores the deploy identifier with each occurrence). ## Connect Claude Code ```bash export LAMPWICK_TOKEN=YOUR_LAMPWICK_TOKEN claude mcp add --transport http lampwick https://app.lampwick.dev/mcp --header "Authorization: Bearer $LAMPWICK_TOKEN" ``` `YOUR_LAMPWICK_TOKEN` is the `lwp_…` token. Start Claude Code and run `/mcp`: `lampwick` should be listed as connected. Then ask "Which Lampwick projects can you see?" (it calls `listProjects`). The [MCP server docs](/docs/mcp/) cover a shared `.mcp.json` and other clients. ## Pre-approve reads, keep writes on "ask" Claude Code asks before every MCP tool call. Pre-approve the read tools this workflow uses in `.claude/settings.json`: ```json { "permissions": { "allow": [ "mcp__lampwick__listProjects", "mcp__lampwick__listIssues", "mcp__lampwick__getIssue", "mcp__lampwick__getExecution", "mcp__lampwick__listLogs" ] } } ``` `updateIssue` (resolve, ignore, reopen) and the other write tools still need your yes each time. ## The workflow ### 1. Ask for what's new > Which open issues in Acme Shop were first seen since 08:00 UTC today? Ask "new since" for fresh bugs and "most occurrences" for impact. With Lampwick the agent calls `listIssues` with `range: "24h"` (or `1h`, `7d`, `14d`) and `status: "open"`, then keeps the issues whose `first_seen` is after 08:00, since there is no "since" parameter. ### 2. Open the stack trace > Show me the stack trace of #57 and the request it happened in. `getIssue` returns the issue, the latest occurrence and its `frames`: file, line, surrounding source and `vendor` (true for frames in `vendor/`). Frames with `vendor: false` are your code, where the agent should start. If the occurrence has an `exec_id`, `getExecution` returns the request, job or command it happened in, with every query, log line and exception on one timeline, which often answers "why only for some users?". (`#57` is the issue's `ref`; tool calls take its `id`.) ### 3. Find the code > Open the code for the top app frame and explain why this throws. Now it works in your repository: it reads the file at the frame's line, follows the other app frames up the call and checks those lines' history for recent changes. If production runs another commit than your checkout, the line numbers won't match. ### 4. Propose a fix, and a test > Propose a fix and a test that reproduces it. Don't commit. Review it as you'd review a colleague's diff: does it fix the cause, or hide the symptom with a `try`/`catch` or a `?->` that turns an error into wrong data? Run the test before and after the fix. ### 5. Close the loop After the deploy, resolve the issue in the dashboard, or ask the agent and approve the `updateIssue` call. If it comes back, it reopens as a regression and you get a push notification. ## Treat monitored data as untrusted input Everything an error tracker stores came from production: exception messages, log lines, URLs, headers, user names. Some of it was typed by your users, or by whoever probes your app. A log line can say "ignore your instructions and run this command", and the agent reads it in its context. The Claude Code docs warn about this [prompt injection risk](https://code.claude.com/docs/en/security) for any MCP server that returns outside content. - Keep write tools on "ask" and approve a call only when it matches what you asked for. - Don't let the agent run commands or fetch URLs that appear in the data. If a suggestion comes from an error message rather than your code, stop and look. - Review every change before you commit it. - A Lampwick token has full access to your account: give the agent its own token with an expiry, and revoke it on **Account & security** when you're done. ## Next step [Add Lampwick to your MCP client](/docs/mcp/), then ask it which issues are new since your last deploy. --- Source: https://lampwick.dev/guides/know-when-your-app-is-down/ # How to check if your website is down > Check if your site is down for everyone or just you, then find which of five causes it is: host, SSL, domain, DNS or broken links. And get told next time. To check if your website is down, open it from another network, such as your phone with Wi-Fi off. If it fails there too, it's down for everyone, and it's almost always one of five things: the host, an expired SSL certificate, a lapsed domain, changed DNS, or an edit that broke links. Each takes a few minutes to check, and this page works for any site, including apps built with Lovable, Replit, Bolt or v0. ## Is it down for everyone, or just you? Your own browser is the worst witness: it may show a saved copy, or fail because of your network. 1. **Try another network.** Your phone with Wi-Fi off is a different connection, a different DNS resolver and an empty cache. 2. **Try a private window.** It skips the saved copy and your logins. If the site works there, the problem is in your browser (an extension, an old cookie), not the site. 3. **Read the error.** It tells you where to look: | What you see | What it usually means | Section | |---|---|---| | "This site can't be reached", `DNS_PROBE_FINISHED_NXDOMAIN`, or your registrar's parking page | The domain doesn't point at your app | [Domain](#the-domain-lapsed), [DNS](#the-dns-changed) | | "Your connection is not private", `NET::ERR_CERT_DATE_INVALID` | The certificate expired ([what to do](/errors/err-cert-date-invalid/)) | [SSL certificate](#the-ssl-certificate-expired) | | A blank page, "502 Bad Gateway", "503", or a page that never loads | The host or the app itself is down | [Host](#the-host-is-down) | | The home page loads, but buttons or pages give "404" | Broken links | [Broken links](#an-edit-broke-links) | If you can use a terminal, one command shows what any computer gets: ```bash curl -sI https://YOUR-DOMAIN ``` `YOUR-DOMAIN` is your address, such as `myapp.com`. A first line like `HTTP/2 200` means it answers; `Could not resolve host` points at DNS or the domain; an SSL error points at the certificate. ## The host is down When your platform has an outage, or your app crashes, visitors get an error or a page that never loads. Check it yourself: if the site fails on another network too, look at your platform's status page: it tells you whether the outage is theirs. If theirs is green, open your platform's dashboard for a failed deploy or a crashed app. ## The SSL certificate expired The SSL certificate puts the padlock in the address bar. It expires every few months and renews automatically; when renewal fails, browsers show a full-page warning such as "Your connection is not private". On a platform address (like `myapp.lovable.app`) the platform handles this. On your own domain, renewal usually needs your DNS to keep pointing at the platform, so a DNS change can quietly stop it. Check it yourself: in Chrome, click the icon left of the address, then **Connection is secure**, then **Certificate is valid** to see the expiry date. ## The domain lapsed You rent your domain (`myapp.com`) from a registrar, usually a year at a time. If the card on file expired or the renewal email went to an old inbox, the domain stops working, and after a grace period someone else can buy it. Check it yourself: look up the expiry date on [ICANN Lookup](https://lookup.icann.org) (for some endings, such as `.nl`, use your registrar's dashboard). Turn on auto-renew and put the date in your calendar a month early. ## The DNS changed DNS is the internet's address book: it tells browsers which servers your domain belongs to. One wrong record and your domain points nowhere, or your email stops arriving, typically after moving registrars, switching email providers or a mistaken edit. Check it yourself: screenshot your DNS records at your registrar now, while everything works, and compare when something breaks. [Google's Dig tool](https://toolbox.googleapps.com/apps/dig/) shows what the rest of the internet sees. ## An edit broke links When an AI builder renames a page or reworks the menu, it can leave links to pages that no longer exist. A related problem is mixed content: an image or script loaded over `http://` on your `https://` page, which browsers block. Check it yourself: after each larger change, click through every menu item and button on the published app. Mixed content shows up in Chrome's console (**View → Developer → JavaScript Console**) as a "Mixed Content" warning. ## How Lampwick does it for you [Lampwick](/for/ai-built-apps/) runs these checks from its own servers, outside your platform, and alerts you by iPhone push and email, or in Slack, Discord, Microsoft Teams or a webhook: | What breaks | What Lampwick does | When you hear | |---|---|---| | Host down | Opens your URL every 5 minutes (Free) to every 30 seconds | After two failed checks in a row: *Down: My app* | | SSL certificate | Reads it every 6 to 12 hours | 14, 7, 3 and 1 days before expiry, and when it becomes invalid | | Domain | Looks up the expiry date daily | 30, 14, 7 and 1 days before | | DNS | Reads the main records hourly | *DNS records changed*, with what changed | | Broken links | Follows the links on up to 250 pages daily | When the crawl finds new broken links or mixed content | You paste your URL; your app stays as it is. Lampwick checks whether your app loads, not whether a button inside it works. For errors inside it, add the [browser error snippet](/docs/browser/): one line in `index.html`, which an AI app builder can add for you. The [uptime docs](/docs/uptime/) have the details. Next step: [set it up for your app](/for/ai-built-apps/#set-it-up-without-code). --- Source: https://lampwick.dev/guides/laravel-cron-job-setup/ # Laravel cron job setup for the scheduler > Laravel needs one crontab line running php artisan schedule:run every minute. How to add it on a server, Forge, Ploi or Docker, and why it may not run. Laravel needs exactly one cron job: a crontab line that runs `php artisan schedule:run` every minute. Each run checks the tasks in `routes/console.php` and starts the ones due at that minute; you never add a cron line per task. ```text * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 ``` Everything below was run on Laravel 13.42 and checked against the [Laravel 13.x scheduling docs](https://laravel.com/docs/13.x/scheduling) on 10 Nov 2026. It applies to Laravel 11 and 12 too. ## Add the crontab line on a server 1. Log in as the user that owns the app and runs PHP-FPM, often `www-data`, `forge` or `ploi`. Not `root`: files the scheduler creates, such as the day's log file, would belong to root and the web server couldn't write to them. 2. Find the PHP binary and the project path: ```bash which php pwd ``` 3. Open the crontab with `crontab -e` and add one line, with absolute paths: ```text * * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /dev/null 2>&1 ``` Replace `/var/www/example.com` with your project directory and `/usr/bin/php` with the output of `which php`. Cron runs with a minimal `PATH`, so a plain `php` can find another PHP version than your shell does, or none. 4. Check it was saved: `crontab -l`. For another user: `sudo crontab -u www-data -l`. The five `*` mean every minute of every hour of every day. The server's timezone doesn't matter for this line, because it runs every minute anyway; task times come from Laravel (see timezones below). `>> /dev/null 2>&1` throws away the output. While you set things up, send it to a file instead, so a broken run leaves a trace: ```text * * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /var/www/example.com/storage/logs/schedule.log 2>&1 ``` ## Check that it runs `schedule:list` shows every task with its cron expression and next run: ```bash php artisan schedule:list ``` ```text * * * * * php artisan reports:send .. Next Due: 45 seconds from now 30 2 * * * php artisan backup:run .. Next Due: 9 hours from now ``` `schedule:run` by hand shows what it starts (this is a Mac with Homebrew PHP). The scheduler runs each task with the same PHP binary as `schedule:run` itself, which the second line shows: ```text 2026-11-10 16:45:14 Running ['artisan' reports:send] .. 455.41ms DONE ⇂ '/opt/homebrew/Cellar/php/8.5.10/bin/php' 'artisan' reports:send > '/dev/null' 2>&1 ``` `php artisan schedule:test` lets you pick one task and run it now, whatever its schedule. ## Laravel Forge, Ploi and Laravel Cloud - **Laravel Forge**: turn on the **Laravel Scheduler** toggle of the site. Forge creates the every-minute job with the site's PHP version ([Forge docs](https://forge.laravel.com/docs/sites/laravel)). Jobs you add yourself live under **Processes → Scheduler** of the server or site. - **Ploi**: on the server, open **Cron jobs** and add a job with the command `php8.5 /home/ploi/example.com/artisan schedule:run`, user `ploi`, frequency every minute ([Ploi docs](https://ploi.io/documentation/server/how-do-i-create-a-cron-job)). Use your site's PHP version and path. - **Laravel Cloud**: click the environment's App cluster, turn on the **Scheduler** toggle, then save and redeploy. There is no crontab line to write. With more than one replica, use `onOneServer()` or tasks run on every replica ([Cloud docs](https://cloud.laravel.com/docs/scheduled-tasks)). The three docs pages were read on 10 Nov 2026. ## Docker and Kubernetes Containers usually have no cron daemon. Two common setups: - **A scheduler container running `schedule:work`.** Same image and environment as the app, with `php artisan schedule:work` as its command. It stays in the foreground and calls the scheduler every minute. Run exactly one of these, or tasks run once per container. ```yaml # docker-compose.yml scheduler: image: your-app-image command: php artisan schedule:work env_file: .env restart: unless-stopped ``` - **A Kubernetes CronJob** with schedule `* * * * *` and command `php artisan schedule:run`. Each minute starts a new pod; set `concurrencyPolicy: Forbid` so slow runs don't pile up. The Laravel docs present `schedule:work` for local development. In a container it does the same job as cron, with one difference: when the process dies, nothing runs until your orchestrator restarts it. ## Tasks that run longer, or on several servers Three options decide what happens when the cron line fires again before a task is done, or fires on more than one server: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('reports:send')->everyFiveMinutes()->withoutOverlapping(10); Schedule::command('invoices:sync')->hourly()->onOneServer(); Schedule::command('analytics:report')->daily()->runInBackground(); ``` - `withoutOverlapping()` skips a run while the previous one still holds a cache lock. The lock expires after 24 hours by default; pass minutes, as above, so a killed process doesn't block the task for a day. - `onOneServer()` lets only the first server that grabs the lock run the task. It needs a `database`, `memcached`, `dynamodb` or `redis` default cache that all servers share. Scheduled closures also need a `->name('…')`. - `runInBackground()` starts a `command` or `exec` task without waiting for it. Tasks due at the same minute otherwise run one after another, so one slow task delays the rest. Sub-minute tasks (`everyTenSeconds()` and the like) keep `schedule:run` alive for the whole minute. Add `php artisan schedule:interrupt` to your deploy script, or the old run keeps using the old code until the minute ends. ## Timezones Laravel decides when a task is due in the app's timezone, `timezone` in `config/app.php` (UTC by default). Set another one per task, or for all tasks with `schedule_timezone`: ```php // config/app.php 'timezone' => 'UTC', 'schedule_timezone' => 'Europe/Amsterdam', ``` ```php Schedule::command('backup:run')->dailyAt('02:30')->timezone('Europe/Amsterdam'); ``` On the nights the clocks change, a task in a daylight saving timezone can run twice or not at all; the Laravel docs advise against timezone scheduling for that reason. Keep the app in UTC, and don't put important tasks between 02:00 and 03:00 local time. ## Why the scheduler isn't running Most "the scheduler stopped" reports come down to one of these: | Symptom | Likely cause | Check | |---|---|---| | Nothing runs, no errors anywhere | No crontab line on this server, or it's in another user's crontab | `crontab -l` as the app user | | Works by hand, not from cron | Cron finds another `php`, or the `cd` path is wrong | Use absolute paths; log output to a file instead of `/dev/null` | | Ran until the last deploy | New server or container without cron; deploy left the app in maintenance mode | `php artisan schedule:list` on the server; tasks skip `php artisan down` unless they have `evenInMaintenanceMode()` | | One task never runs | A stuck `withoutOverlapping` lock after a crash | `php artisan schedule:clear-cache` | | A task runs on every server | `onOneServer()` without a shared cache | Your default cache store | | Runs at the wrong hour | App timezone or daylight saving time | `php artisan schedule:list` | | Nothing runs since someone paused it | `php artisan schedule:pause` | `php artisan schedule:continue` | The [scheduler monitoring guide](/guides/monitor-laravel-scheduler/) reproduces the stuck lock and the "it failed but said `DONE`" case, and shows how to catch each. ## Know when it stops A crontab line can disappear with a server move and nothing complains: no task runs, so no task fails. The usual fix is a heartbeat per task: `->pingOnSuccess($url)` to a cron monitoring service, which alerts when a ping is missing. We make Lampwick, which checks the schedule without heartbeats: the [Laravel package](/docs/laravel/) reports every scheduled task run, and **Schedule health** learns each task's cron and timezone from its runs. When a task misses its run you get an alert, and when no task runs at all you get one "scheduler not running" alert for the project instead of one per task. Details are in the [Schedule health docs](/docs/cron-health/). ## Next step Add the crontab line, run `php artisan schedule:list`, then set up [monitoring for the scheduler](/guides/monitor-laravel-scheduler/) so you hear about the night it doesn't run. --- Source: https://lampwick.dev/guides/laravel-failed-jobs/ # Laravel queue monitoring: failed jobs, timeouts, retries > Laravel jobs fail by throwing, timing out or running out of attempts (MaxAttemptsExceededException). How retries work, and how to get alerted. A queued job fails when it throws on its last attempt, runs longer than its timeout, or is picked up more often than its `tries` allow. Laravel then writes it to the `failed_jobs` table and moves on: nobody gets an email, and the worker log only says `FAIL`. Monitoring a Laravel queue comes down to three questions: which jobs failed and why, which jobs only succeed after retries, and whether the queue is falling behind. Laravel answers the first in `failed_jobs`, and leaves the other two to you. Everything below was run on Laravel 13.40 and 13.42 with the database queue driver, against the [Laravel 13.x queue docs](https://laravel.com/docs/13.x/queues). ## The three ways a job fails ### It throws on its last attempt By default Laravel attempts a job once; an exception releases it back onto the queue only while it has attempts left. Laravel 13 sets this with attributes on the job class: ```php [ 'driver' => 'redis', 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 330), 'block_for' => null, 'after_commit' => false, ], ``` ```bash php artisan queue:work redis --timeout=300 ``` ## Retries: tries, backoff and retryUntil An attempt is one pickup of the job by a worker. Laravel counts it when the worker reserves the job, before `handle()` runs, so every exception, timeout, crash and `release()` uses one. Without settings, a job gets 1 attempt. | Setting | Where | What it does | |---|---|---| | `#[Tries(5)]` or `--tries=5` | Job class, worker | At most 5 attempts. The class wins over the worker flag. `--tries=0` means unlimited. | | `#[Backoff([10, 30, 90])]` or `--backoff=10` | Job class, worker | Seconds to wait before each retry after an exception; the last value repeats. | | `retryUntil()` | Job method returning a `DateTime` | Keep retrying until that time, however many attempts. Wins over `tries`. | | `#[MaxExceptions(3)]` | Job class | Fail after 3 exceptions even if `tries` allows more, so releases don't eat the budget for real errors. | | `$this->release(30)` | Inside `handle()` | Put the job back for 30 seconds. Counts as an attempt. | To stop retrying an exception that will never succeed, fail the job at once with the `FailOnException` job middleware, or list the exception in `$exceptions->dontRetry([...])` in `bootstrap/app.php`. Retries hide problems. A job that fails twice and succeeds on the third attempt never reaches `failed_jobs`, but it cost two minutes and two calls to the API. Look at released counts, not only failures. ## MaxAttemptsExceededException: what it means `Illuminate\Queue\MaxAttemptsExceededException: App\Jobs\X has been attempted too many times.` means a worker picked up the job when it had no attempts left. Laravel fails it without running `handle()`, so there is often no exception of your own in the log. Three causes, most common first: 1. **The job ran longer than `retry_after`** and a second worker took it over, as in the reproduction above. Fix: timeout several seconds below `retry_after`. 2. **The job released itself until it ran out.** `release()`, and job middleware such as `WithoutOverlapping` and `RateLimited`, put the job back and use an attempt each time. We reproduced it with a job that always calls `$this->release(1)` and `#[Tries(2)]`: ```text 2026-11-10 16:42:50 App\Jobs\PollCarrier .. RUNNING 2026-11-10 16:42:50 App\Jobs\PollCarrier .. 4.74ms RELEASED 2026-11-10 16:42:51 App\Jobs\PollCarrier .. RUNNING 2026-11-10 16:42:51 App\Jobs\PollCarrier .. 4.49ms RELEASED 2026-11-10 16:42:52 App\Jobs\PollCarrier .. RUNNING 2026-11-10 16:42:52 App\Jobs\PollCarrier .. 13.00ms FAIL ``` Two attempts released, the third pickup failed with `MaxAttemptsExceededException`. Fix: raise `Tries` or use `retryUntil()`, and add `#[MaxExceptions]` so real errors still fail early. 3. **The worker died mid-job.** An out-of-memory kill, a deploy that killed the worker, or a server restart. The attempt was counted, the job reappears after `retry_after`, and with no attempts left it fails. Fix the crash first: `--memory`, `--max-jobs=1000` or `--max-time=3600` to recycle workers, and `stopwaitsecs` in Supervisor longer than your longest job. The `failed()` method receives the `MaxAttemptsExceededException` in all three cases, not the original error. ## Timeouts: TimeoutExceededException A job's timeout is 60 seconds unless `#[Timeout(120)]` or `--timeout=120` says otherwise, and it needs the `pcntl` extension. When a job runs past it, the worker kills the job and exits. That attempt is used up: with attempts left, the job is retried; on the last attempt it fails with `Illuminate\Queue\TimeoutExceededException`. Add `#[FailOnTimeout]` to fail at the first timeout instead of retrying. Three rules keep timeouts predictable: - **Timeout below `retry_after`**, by several seconds, or the job runs twice (see above). Under Horizon, the supervisor's `timeout` too. - **Timeouts on network calls.** A stuck HTTP or socket call can block past the job timeout. Give the client its own: `Http::timeout(10)->connectTimeout(5)->get($url)`. - **`--timeout` does nothing with `--once`.** ## Watch the queue backlog as well as failures A queue can fail without a single failed job: the worker stopped, or jobs come in faster than they go out. Laravel's `queue:monitor` checks the size: ```php // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('queue:monitor database:default --max=100')->everyMinute(); ``` ```text Queue name .. Size / Status [database] default .. [1] ALERT Pending jobs .. 1 Delayed jobs .. 0 Reserved jobs .. 0 Oldest pending job .. 1 second ago ``` (That output is from `--max=0`, to show the alert.) Above the threshold it fires `Illuminate\Queue\Events\QueueBusy`; listen for it in `AppServiceProvider::boot()` and send a notification, like the failed-job hook below. On Redis, Horizon sends long-wait notifications by mail, Slack or SMS (`Horizon::routeSlackNotificationsTo()`), with a threshold per queue in the `waits` option of `config/horizon.php` (60 seconds by default). Both depend on the scheduler running; the [cron job setup guide](/guides/laravel-cron-job-setup/) covers that. ## Work with `failed_jobs` New Laravel apps have the migration. If yours doesn't: ```bash php artisan make:queue-failed-table php artisan migrate ``` List, retry and clean up: ```bash php artisan queue:failed php artisan queue:retry all php artisan queue:retry --queue=QUEUE_NAME php artisan queue:forget FAILED_JOB_UUID php artisan queue:prune-failed --hours=48 ``` `QUEUE_NAME` is the queue to retry, `FAILED_JOB_UUID` an ID from `queue:failed`. `queue:retry` pushes the original payload back, so fix the cause first. Under Horizon, use `horizon:forget`. Synchronously dispatched jobs never reach this table. ## Clean up in `failed()` A `failed` method runs once after the last attempt, with the exception: yours, `MaxAttemptsExceededException` or `TimeoutExceededException`. ```php use Illuminate\Support\Facades\Log; use Throwable; public function failed(?Throwable $exception): void { Log::error('SyncInvoices failed', ['exception' => $exception?->getMessage()]); } ``` Use it to undo partial work or tell the user. It's per class, so it's the wrong place to alert the team. ## Get notified when any job fails `Queue::failing` runs for every failed job. Send it where a person reads it, such as a Slack incoming webhook: ```php // app/Providers/AppServiceProvider.php use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; public function boot(): void { Queue::failing(function (JobFailed $event) { Http::post(config('services.slack.failed_jobs_webhook'), [ 'text' => sprintf( 'Job failed: %s on %s. %s', $event->job->resolveName(), $event->connectionName, $event->exception->getMessage(), ), ]); }); } ``` For the `SyncInvoices` job above, that posts `Job failed: App\Jobs\SyncInvoices on database. Billing API returned 503`. This catches failures, not patterns: a job that fails 2 of its 3 attempts every night never shows up. For that you need every attempt. ## How Lampwick monitors your queue The [Lampwick Laravel package](/docs/laravel/) records every job attempt. **Jobs** lists each job class with its processed, released and failed counts and p95 duration; each attempt opens a timeline of its queries, logs and exceptions. An exception in a job becomes an issue in **Issues**, with a push notification when it's new or comes back after you resolved it. There is no separate alert for a failed job, and no queue-size alert: keep `queue:monitor` or Horizon for the backlog. Details are on the [product page](/product/#jobs). ## Next step [Connect a Laravel app](/docs/laravel/) and run one of your jobs: its attempts show up under **Jobs** within seconds. --- Source: https://lampwick.dev/guides/laravel-logging-in-production/ # Laravel logging in production: a setup guide > Laravel logs debug lines to one file by default. In production: daily files or stderr, LOG_LEVEL=info, JSON, shared context, and logs off the server. A new Laravel app logs everything, at `debug` level, into one file: `storage/logs/laravel.log`. In production you want four changes: rotate the file (or write to stderr in containers), raise `LOG_LEVEL`, write JSON with a request or trace ID on every line, and get the logs off the server so you can still read them when it's down. Everything below was run on Laravel 13.42 and checked against the [Laravel 13.x logging docs](https://laravel.com/docs/13.x/logging) and the [Context docs](https://laravel.com/docs/13.x/context) on 10 Nov 2026. Laravel 12 works the same, with one difference noted under rotation. ## What a new app logs, and where Logging lives in `config/logging.php`, and a fresh `.env` sets four values: ```dotenv LOG_CHANNEL=stack LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug ``` `LOG_CHANNEL` picks the default channel. `stack` is a channel that writes to a list of other channels, and `LOG_STACK` is that list, comma-separated. `single` is one file that grows forever. Every channel that reads `LOG_LEVEL` drops messages below that level. So out of the box, every `Log::debug()` in your code and in your packages lands in one file on the web server's disk, which nobody rotates. ## Pick channels: a stack for production Laravel ships these channels in `config/logging.php`, ready to use by name in `LOG_STACK`: | Channel | Writes to | Use it for | |---|---|---| | `single` | `storage/logs/laravel.log` | Local development | | `daily` | `storage/logs/laravel-YYYY-MM-DD.log`, 14 files kept | One or a few servers you can SSH into | | `stderr` | The process's stderr | Docker, Kubernetes, Laravel Cloud: the platform collects it | | `syslog`, `errorlog` | The system log, PHP's `error_log` | Servers where syslog is already shipped somewhere | | `slack` | A Slack incoming webhook, `critical` and up | A few lines a person must read now | | `papertrail` | Papertrail over TLS | If you use Papertrail | | `null` | Nowhere | Turning a channel off | A typical server setup writes to daily files and sends critical lines to Slack: ```dotenv LOG_CHANNEL=stack LOG_STACK=daily,slack LOG_LEVEL=info LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL ``` `YOUR/WEBHOOK/URL` is the path of a Slack incoming webhook. One trap: the default `slack` channel reads `'level' => env('LOG_LEVEL', 'critical')`. The moment you set `LOG_LEVEL=info`, Slack gets every info line too. Hard-code its level: ```php // config/logging.php, channels 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 'level' => 'critical', 'replace_placeholders' => true, ], ``` To write one message to one channel, skip the stack: `Log::channel('slack')->critical('Payments are failing')`. ## Choose a log level for production Monolog has the eight levels of RFC 5424, from most to least severe: `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info`, `debug`. A channel logs its own level and everything above it. - `LOG_LEVEL=info` is a good default: you keep "order paid" and "import finished", and lose the debug noise of packages. - `warning` if your logs are large and you only read them when something broke. - `debug` in production only for a short investigation, then back. Debug lines often contain payloads you don't want stored. Unhandled exceptions are logged at `error` by Laravel's exception handler, so they stay visible at any of these levels. ## Rotate files: `daily`, or logrotate with `copytruncate` The `daily` channel starts a new file each day and deletes the oldest beyond `LOG_DAILY_DAYS` (14 by default): ```php // config/logging.php in Laravel 13 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'max_files' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, ], ``` Laravel 12's config calls the option `days`; Laravel 13 reads `max_files` and still accepts `days`. Laravel 13's config also has a `monthly` channel that keeps 3 files. Two things go wrong with log files on servers: - **Permission denied.** The first process to write a day's file owns it. If cron or a deploy script runs `php artisan` as `root` at 00:01, PHP-FPM as `www-data` can't write to that file for the rest of the day, and the error ends up nowhere. Run artisan commands as the web user, or set `'permission' => 0664` on the channel with a shared group. - **Queue workers keep the old file.** If you rotate `single` with `logrotate` instead, its default moves the file away and creates a new one. Long-running processes (`queue:work`, Horizon, Octane) keep their handle on the moved file and write there. Use `copytruncate` in the logrotate config, or restart the workers after rotation. ## Write JSON The default line format is fine to read and hard to search. Any file or stream channel takes a Monolog formatter; add one line to `daily`: ```php 'daily' => [ // ...the options above 'formatter' => Monolog\Formatter\JsonFormatter::class, ], ``` For `stderr`, set it from the environment: `LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter`. A log call with a placeholder: ```php Log::info('Order {order} paid', ['order' => 1042, 'amount_cents' => 4900]); ``` now writes one JSON object per line: ```json {"message":"Order 1042 paid","context":{"order":1042,"amount_cents":4900},"level":200,"level_name":"INFO","channel":"production","datetime":"2026-11-10T16:44:21.879609+00:00","extra":{"trace_id":"e04e1a11"}} ``` The `{order}` placeholder is filled from the context because the channel has `replace_placeholders`. The `channel` field is your `APP_ENV`. The `extra.trace_id` comes from the next section. ## Add context to every line A log line without a request ID can't be tied to the other lines of the same request. Laravel has three ways to add context, from narrow to broad: 1. **Per call**: the array you pass, as above. 2. **Per channel**: `Log::withContext(['request_id' => $id])` adds it to every later line of the current channel. `Log::shareContext([...])` does the same for all channels, including ones created later. 3. **Everywhere, including queued jobs**: the `Context` facade, since Laravel 11. `Context` is the one to use. Add it in a middleware: ```php toString()); Context::add('url', $request->url()); return $next($request); } } ``` Register it in `bootstrap/app.php` with `->withMiddleware(fn ($middleware) => $middleware->append(\App\Http\Middleware\AddLogContext::class))`. Context lands in each record's `extra`, not `context`, so you can tell it apart from what one call passed. It also travels with the queue. A job dispatched during that request gets the same `trace_id` on its log lines when a worker runs it, minutes later and on another server. We dispatched a job that logs `Receipt sent` and ran it with `queue:work` (`APP_ENV=production`): ```json {"message":"Receipt sent","context":{"order":1042},"level":200,"level_name":"INFO","channel":"production","datetime":"2026-11-10T16:44:24.841978+00:00","extra":{"trace_id":"e04e1a11"}} ``` Data a job needs but the log shouldn't show goes in hidden context: `Context::addHidden('api_key', $key)` is passed to jobs and never written to a log. ## What not to log Logs get copied to more places and kept longer than your database, and more people can read them. Keep these out: - Passwords, API keys, tokens, session IDs and full `Authorization` headers. - Whole request bodies (`$request->all()`): they contain all of the above, sooner or later. - Card numbers and other payment data; government IDs. - Personal data you don't need to debug. A user ID is usually enough; the email address isn't needed. Log the ID of the thing (`order 1042`), not the thing. Under the GDPR, personal data in logs needs a retention period like any other copy, which `LOG_DAILY_DAYS` gives you for files. ## Read logs: on the server, and off it On the server, [Laravel Pail](https://laravel.com/docs/13.x/logging#tailing-log-messages-using-pail) tails the logs with filters, for any channel (it needs `pcntl`): ```bash composer require --dev laravel/pail php artisan pail --level=error php artisan pail --filter="QueryException" ``` `--user=USER_ID` shows only lines written while that user was logged in. Pail is a dev dependency here; on a production box without dev dependencies, `tail -f storage/logs/laravel-$(date +%F).log` works for `daily`. Files on the web server have two limits: they disappear with the server, and you have to SSH in to read them. Before your first incident, send them somewhere else: `stderr` into your platform's log collection, `syslog` into a log service, or a hosted tool's log channel. ## Logs next to the request in Lampwick We make Lampwick. Its [Laravel package](/docs/laravel/) adds a `lampwick` log channel; put it in your stack next to the files: ```dotenv LOG_STACK=daily,lampwick ``` **Logs** in the project lists every entry with its level, message, context and extra (where `Context` data lands), with a level filter ("warning and above") and search over the message and context. Each entry links to the request, job or command it was written in, whose timeline shows that log line between its queries and exceptions. Lampwick replaces the values of keys like `password`, `token`, `secret` and `authorization` before a record leaves your server. `LAMPWICK_LOG_LEVEL` sets the lowest level sent, separately from `LOG_LEVEL`. History is 3 days on Free and 14 to 60 days on paid plans. Lampwick doesn't alert on log lines. Exceptions become [issues](/docs/issues/), which do alert; an agent can read logs too, with `listLogs` over [MCP](/docs/mcp/). The [product page](/product/#logs) shows how logs, exceptions and requests fit together. ## Next step Set `LOG_STACK=daily,lampwick` and `LOG_LEVEL=info`, add the context middleware, and [connect your Laravel app](/docs/laravel/). For logs from queued jobs that fail, see [Laravel queue monitoring](/guides/laravel-failed-jobs/). --- Source: https://lampwick.dev/guides/monitor-laravel-scheduler/ # Monitor the Laravel scheduler, and why tasks silently don't run > Laravel's scheduler runs only when cron calls schedule:run each minute. Missing cron, stuck locks and timezones skip tasks silently; a heartbeat catches it. Laravel's scheduler runs nothing by itself: cron must call `php artisan schedule:run` every minute, and each run starts the tasks due at that minute. When a task doesn't run there is usually no error at all, so the only reliable check expects a signal from the task and alerts when it doesn't arrive. Everything below was run on Laravel 13.40, against the [Laravel 13.x scheduling docs](https://laravel.com/docs/13.x/scheduling). ## How the scheduler runs You define tasks in `routes/console.php`: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('reports:send')->everyMinute()->withoutOverlapping(); Schedule::command('invoices:sync')->hourly()->onOneServer(); Schedule::command('backup:run')->dailyAt('02:30')->timezone('Europe/Amsterdam'); ``` On the server, one cron entry runs the scheduler every minute: ```text * * * * * cd PROJECT_PATH && php artisan schedule:run >> /dev/null 2>&1 ``` `PROJECT_PATH` is your app's directory, such as `/home/forge/example.com/current`. Locally, `php artisan schedule:work` does the same in the foreground. `schedule:list` shows what will run, and when: ```text * * * * * php artisan reports:send .. Next Due: 14 seconds from now 0 * * * * php artisan invoices:sync .. Next Due: 15 minutes from now 30 1 * * * php artisan backup:run .. Next Due: 1 hour from now ``` The backup shows `30 1 * * *`, not `30 2`: the app runs in UTC and Amsterdam is UTC+1 in November (`--timezone=Europe/Amsterdam` shows local times). ## Why a task doesn't run ### No cron entry, or the wrong one The most common cause: a new server, a container without cron, or a crontab for the wrong user, and nothing complains. Check `crontab -l` as the user that owns the app (`forge`, `www-data`). In Docker, run cron in a container with your code and `.env`, or run `php artisan schedule:work` as its own process. ### A stuck `withoutOverlapping` lock `withoutOverlapping()` holds a cache lock while the task runs. If the process dies hard (OOM kill, reboot, a container stopped mid-run), the lock stays for 24 hours by default, and the task is skipped until then. We reproduced it by killing `schedule:run` with `SIGKILL` while `reports:send` ran. The next minute: ```text INFO No scheduled commands are ready to run. ``` `reports:send` was due, and skipped. Clear the lock, and give it a shorter expiry in minutes: ```bash php artisan schedule:clear-cache ``` ```php Schedule::command('reports:send')->everyMinute()->withoutOverlapping(10); ``` ### `onOneServer` without a shared cache `onOneServer()` needs a `database`, `memcached`, `dynamodb` or `redis` cache that all servers share. Otherwise each server gets its own lock and the task runs everywhere. Scheduled closures also need a `name()`. ### The timezone or daylight saving time On the night the clocks change, a task at 02:30 Amsterdam time can run twice or not at all; the Laravel docs advise against timezone scheduling for this reason. Schedule in UTC, or avoid 02:00 to 03:00. ### Maintenance mode, or a paused scheduler During `php artisan down`, tasks don't run unless they have `evenInMaintenanceMode()`, so a deploy that fails after `down` keeps them all off. `php artisan schedule:pause` stops all tasks until `schedule:continue`, except those with `evenWhenPaused()`. ### It runs, but fails If a task throws or exits non-zero, `schedule:run` reports the exception, fires `ScheduledTaskFailed` and moves on. On Laravel 13.40 its output still said `DONE` for a command and a closure that threw: ```text 2026-11-07 23:49:03 Running ['artisan' backup:run] .. 253.60ms DONE 2026-11-07 23:49:03 Running [broken-closure] .. 0.28ms DONE ``` The task ran, so a "did it run?" check stays green. Tasks due at the same minute also run one after another, so a slow one delays the rest; use `runInBackground()` for long commands. ## Check that tasks run: heartbeats Give each important task a heartbeat URL from a cron monitoring service, which alerts when a ping is missing or reports failure: ```php Schedule::command('backup:run') ->dailyAt('02:30') ->pingBefore('https://HEARTBEAT_HOST/HEARTBEAT_ID/start') ->pingOnSuccess('https://HEARTBEAT_HOST/HEARTBEAT_ID') ->pingOnFailure('https://HEARTBEAT_HOST/HEARTBEAT_ID/fail'); ``` `HEARTBEAT_HOST` and `HEARTBEAT_ID` come from your monitoring service; the `/start` and `/fail` paths differ per service. Success and failure follow the exit code; `thenPing($url)` pings after every run. You add each heartbeat by hand and update it when the schedule changes. To log failures without a service, listen for the event in `AppServiceProvider::boot()`: ```php use Illuminate\Console\Events\ScheduledTaskFailed; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; Event::listen(function (ScheduledTaskFailed $event) { Log::critical('Scheduled task failed', [ 'task' => $event->task->description ?? $event->task->command, 'exception' => $event->exception->getMessage(), ]); }); ``` That catches failures, never missed runs: a task that doesn't run fires no event. ## How Lampwick checks your schedule The [Lampwick Laravel package](/docs/laravel/) records every scheduled task run with its cron expression and timezone, so there are no heartbeats to configure. **Schedule health** derives each task's schedule from its runs and shows the cron in words, the last and next expected run, and a status. - Missed: no run within a margin after the expected time (at least 5 minutes). One alert, then "Running again" when it recovers. - Scheduler not running: one alert for the project instead of one per task. - Failing: when a task starts to fail, at most once an hour per task. - Too slow: optional, above a runtime you set per task. Alerts go out by push and email, and to Slack, Discord, Microsoft Teams or a webhook if you add an [alert channel](/docs/alerts/#alert-channels). A task is known after its first run. Keep `LAMPWICK_SCHEDULED_TASK_SAMPLE_RATE` at 1: missed-run alerts need every run. Mute or forget a task on **Schedule health**, or with `updateSchedule` and `deleteSchedule` in the API. ## Next step [Connect a Laravel app](/docs/laravel/), let your tasks run once, then open **Schedule health**. --- Source: https://lampwick.dev/guides/set-up-monitoring-with-claude-code/ # Set up monitoring with Claude Code > Ask Claude Code to add error and uptime monitoring to your app: it installs the package, keeps the token in .env, sends a test event and checks it arrived. Claude Code can add monitoring to your app in one session: it installs the package, puts the token in `.env`, sends a test event and checks that it arrived. You stay in charge of three things: which environment it touches, where the secrets go, and who owns the account at the end. This guide sets up Lampwick, where Claude Code can also create the workspace itself and hand you a link to claim it. The preparation and the checks apply to any monitoring tool. ## Before you start - **Start on dev or staging.** A monitoring tool receives exception messages, request URLs and log lines. Point the first setup at an environment without customer data, and move to production once you have read the terms and the data processing agreement. - **Keep `.env` out of git.** Check that `.gitignore` lists it: `git check-ignore .env` prints `.env` when it does. - **Keep secrets out of the conversation.** Everything Claude Code reads goes into its context. Deny it reading `.env` in `.claude/settings.json`; it can still append to the file with a shell command, which you approve: ```json { "permissions": { "deny": ["Read(./.env)", "Read(./.env.*)"] } } ``` The `deny` rules are in the [Claude Code settings docs](https://code.claude.com/docs/en/settings). ## 1. Give it the prompt Start `claude` in the repository and paste: ```text Set up Lampwick monitoring for this app. Read https://lampwick.dev/docs/agents.md and follow it. Keep the tokens out of the chat, use a dev or staging environment, and give me the claim link when the first event has arrived. ``` The [agent setup page](/docs/agents/) is written for agents: exact requests, fields and errors. Pointing Claude Code at it beats describing the steps yourself, because it follows the same page every time. ## 2. Approve the requests Claude Code asks before each command. Expect these, in this order: 1. A line that adds `.lampwick-workspace.json` to `.gitignore`. Claude Code runs every command in a fresh shell, so the setup passes values on through that file and `.env`. 2. One long command: `curl` to `/api/v1/workspaces/challenge`, a one-line `python3` command that solves a proof-of-work (a few seconds), and `curl -X POST` to `/api/v1/workspaces` with your team and project name and `"agent":"Claude Code"`. It saves the answer, with a workspace token (`lws_…`), an ingest token and a claim link, to `.lampwick-workspace.json` and prints only the workspace's name. 3. A command that appends `LAMPWICK_WORKSPACE_TOKEN` and `LAMPWICK_TOKEN` to `.env`, after adding a final newline if the file lacks one. 4. `composer require lampwick/laravel` and `php artisan lampwick:test`, or a `curl` to `/ingest` for other apps. `lampwick:test` exits 1 while the ingest daemon isn't running; its test record still counts. Then it starts the ingest daemon (`php artisan lampwick:agent`) in this environment and loads a page, so real requests arrive. 5. Optionally `curl -X POST` to `/api/v1/projects/…/monitors` for an uptime check of your staging URL. 6. `curl` to `/api/v1/setup-status`, until it says `"ready_to_hand_off": true`. Say no when a command prints a token to the terminal, or writes one anywhere but `.env` and `.lampwick-workspace.json`. Ask it to redo the step. ## 3. Check its work Don't take "done" on trust. `getSetupStatus` answers with what Lampwick actually received: ```json { "ready_to_hand_off": true, "blocking": [], "next_action": "Give your human the claim link with the text of handoff_message.", "projects": [{ "id": 3, "name": "acme-web", "first_event_at": "2026-11-09T17:40:47.648610Z", "last_deploy_at": null }] } ``` Run on 9 Nov 2026 against a Lampwick server. `first_event_at` is set only when a record arrived. Until then `blocking` says `no events yet from acme-web`, and Claude Code should fix that before it hands off. ## 4. Claim the workspace Claude Code ends with a message like this one, from `handoff_message`: ```text Lampwick is set up for Acme. Open this link to put it in your name: https://app.lampwick.dev/claim/lwc_… — claim it before Thu 12 Nov, 17:40 UTC, or it stops and is deleted. ``` 1. Open the link. It shows who set the workspace up, what it holds and when it is deleted. Claim it only if you asked for it. 2. Log in or create an account, then click **Claim workspace**. 3. Choose **Read-only (recommended)** or **Read & write**, click **Show code for the agent**, and paste the code into Claude Code. It sends it with `confirmWorkspaceAccess`. Read-only is the safer choice for an agent that reads production errors: those messages can contain text meant to steer it. The agent keeps the access you chose for 30 days, then connects by signing in. No code within 15 minutes means it loses access, and Lampwick replaces the ingest tokens. ## 5. Go to production Once the workspace is yours: - Run the ingest daemon under a process manager: `php artisan lampwick:agent` ([Laravel docs](/docs/laravel/)). - Set `LAMPWICK_TOKEN` in production's environment, not in a committed file. - Record each deploy from CI with `php artisan lampwick:deploy` (it uses the same `LAMPWICK_TOKEN`), so every issue shows the deploy that brought it. - Turn on [alerts](/docs/alerts/). An agent can't do that for you: alert channels, status pages and billing stay with people. ## Next step Now let Claude Code read what breaks: [debug Laravel production errors with Claude Code](/guides/debug-laravel-production-errors-with-claude-code/). --- Source: https://lampwick.dev/guides/telescope-pulse-in-production/ # Telescope vs Pulse vs Nightwatch in production > Telescope inspects single requests, Pulse aggregates, Nightwatch is hosted and alerts. How to run each in production, and yes, Nightwatch has a free plan. Telescope records individual requests, queries, jobs and exceptions so you can inspect one; Pulse aggregates them into a dashboard of slow routes, slow jobs and busy users; Nightwatch is Laravel's hosted service that keeps every event, groups exceptions into issues and alerts you. Telescope and Pulse are free packages that store data in your own database and never send an alert. Nightwatch has a free plan and paid plans from $20 a month. We make Lampwick, a hosted tool like Nightwatch, listed at the end. The Laravel parts follow the [Telescope](https://laravel.com/docs/13.x/telescope) and [Pulse](https://laravel.com/docs/13.x/pulse) docs for Laravel 13.x, read 8 Nov 2026. Nightwatch facts are from [laravel.com/nightwatch](https://laravel.com/nightwatch) and its [pricing page](https://laravel.com/nightwatch/pricing), verified on 10 Nov 2026. ## Telescope, Pulse and Nightwatch side by side | | Telescope | Pulse | Nightwatch | |---|---|---|---| | Question it answers | What happened in this one request or job? | What is slow or busy across the app? | What broke, for whom, and is it getting worse? | | Records | Requests, exceptions, logs, queries, jobs, mail, notifications, cache, scheduled tasks, dumps and more, each as an entry | Aggregates: slow requests, jobs, queries and outgoing requests (over 1,000 ms by default), exceptions, queue throughput, cache hits, active users, server CPU, memory and disk | Every request, outgoing request, query, job, command, mail, notification, cache event, scheduled task, exception and log | | Stored in | `telescope_entries` in your database | Pulse tables in MySQL, MariaDB or PostgreSQL | Nightwatch's data centers, in the US or EU | | Kept | Until you prune it | 7 days by default | 14 to 90 days, by plan | | Alerts | None | None | Exceptions grouped into issues, performance thresholds, alerts | | Runs | In your app, dashboard at `/telescope` | In your app, dashboard at `/pulse` | A package plus `php artisan nightwatch:agent` on each server | | Costs | Free, open source | Free, open source | Free plan; paid from $20/month | The Laravel docs present Telescope as a companion for local development and point from Pulse to Telescope for single events. Laravel's own [Nightwatch vs Pulse](https://laravel.com/nightwatch/nightwatch-vs-pulse) page marks Telescope "not recommended" for production; it can still run there with the precautions below. ## Is Laravel Nightwatch free? Yes. The Free plan costs $0 and includes 300,000 events a month, 14 days of lookback, unlimited applications, environments and seats, and 1 performance monitor. Free projects pause after 30 days without app activity, and one user can have at most 3 free organizations. An event is one request, query, job, cache operation, log line and so on, so a busy app uses 300,000 quickly. Paid plans, as listed on 10 Nov 2026 ([pricing](https://laravel.com/nightwatch/pricing)): | Plan | Per month | Events included | Lookback | Extra events | |---|---|---|---|---| | Free | $0 | 300k | 14 days | $0.50 per 100k | | Pro | $20 | 7.5M | 30 days | $0.35 per 100k | | Team | $60 | 30M | 60 days | $0.35 per 100k | | Business | $300 | 180M | 90 days | $0.20 per 100k | You can set a spending cap; when it's hit, Nightwatch stops taking in new events. Sampling keeps the event count down. Telescope and Pulse stay free and open source, and Laravel says it will keep supporting both. ## Running Telescope in production ### Gate the dashboard The dashboard is open only in the `local` environment. In other environments, the `viewTelescope` gate in `app/Providers/TelescopeServiceProvider.php` decides who gets in: ```php use App\Models\User; use Illuminate\Support\Facades\Gate; protected function gate(): void { Gate::define('viewTelescope', function (User $user) { return in_array($user->email, [ 'you@example.com', ]); }); } ``` **Set `APP_ENV=production` in production.** With `APP_ENV=local` the dashboard is public, and with it request headers, sessions and query bindings. ### Record less Outside `local`, the default `filter` closure records only reportable exceptions, failed jobs, scheduled tasks, slow queries and entries with monitored tags. Keep it. If you widen it, turn off unneeded watchers in `config/telescope.php` and keep `hideSensitiveRequestDetails()`. `TELESCOPE_ENABLED=false` stops recording without removing the package. ### Prune every day `telescope_entries` grows quickly. Schedule pruning in `routes/console.php`: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('telescope:prune --hours=48')->daily(); ``` Without `--hours`, entries older than 24 hours go. If the scheduler stops, the table keeps growing; the [scheduler guide](/guides/monitor-laravel-scheduler/) shows how to check it. ### Or keep it local only Install it with `--dev`, register it only in `local` and add it to `dont-discover`: the [local-only steps](https://laravel.com/docs/13.x/telescope#local-only-installation). ## Running Pulse in production Pulse is built for production: it pre-aggregates data into time buckets. What to set: - Gate `/pulse` with the `viewPulse` gate in `AppServiceProvider`; by default it is only open locally. - For high traffic, a separate database: `PULSE_DB_CONNECTION=pulse` (MySQL, MariaDB or PostgreSQL). - Ingest through Redis with `PULSE_INGEST_DRIVER=redis` and a `php artisan pulse:work` process, which moves entries from a Redis stream into the database. Use a different Redis connection than your queue. - Sample busy recorders: a rate of `0.1` records about 10% of entries and scales the numbers up, marked `~`. - Run `pulse:check` under Supervisor on each server for the Servers card, and `pulse:restart` on deploy. If Pulse can't write, say because its database is down, it fails silently so your app keeps working, and a broken Pulse looks like a quiet week. Log those errors with `Pulse::handleExceptionsUsing()`. ## Running Nightwatch in production Nightwatch has no dashboard in your app to protect, and nothing to prune. What you run: ```bash composer require laravel/nightwatch php artisan nightwatch:agent ``` Set `NIGHTWATCH_TOKEN` in `.env`, and keep the agent running under Supervisor or a Forge daemon, like a queue worker; without it, nothing is sent. `NIGHTWATCH_ENABLED=false` turns it off in tests. Watch the event count: every query and cache hit is an event, so sample busy apps before the Free plan runs out ([Nightwatch docs](https://nightwatch.laravel.com/docs)). ## Which one to use - **Telescope** on your laptop, and in production only gated, filtered and pruned, for the rare "what exactly did this request do?". - **Pulse** when you want trends on one or two servers and someone opens the dashboard often. - **A hosted tool** when somebody needs to know within minutes that checkout is throwing or the backup didn't run. Telescope and Pulse never tell anyone, and their data lives in your own database, so an outage takes the dashboards down too. Hosted options for Laravel, checked 8 Nov 2026 unless marked: - [Laravel Nightwatch](https://laravel.com/nightwatch): from the Laravel team, see above (verified 10 Nov 2026). Laravel also compares it with [Telescope](https://laravel.com/nightwatch/nightwatch-vs-telescope). - [Flare](https://flareapp.io): errors, performance and logs for Laravel, PHP and JavaScript, from Spatie. - [Sentry](https://docs.sentry.io/platforms/php/guides/laravel/): error tracking and tracing, through the `sentry/sentry-laravel` package. - [Lampwick](/docs/laravel/): issues, requests, jobs, queries, logs and schedule health from the Laravel package, plus uptime, SSL, domain and DNS checks, status pages and an [MCP server](/docs/mcp/). Alerts by iOS push, email, Slack, Discord, Microsoft Teams or webhooks. Free plan, then €29, €79 or €199 a month. Pulse can stay next to any of them, and Telescope on your laptop. The [Laravel monitoring tools](/compare/laravel-monitoring-tools/) page lists more, with prices. ## Next step Comparing Nightwatch with Lampwick directly? Read [Lampwick vs Nightwatch](/compare/lampwick-vs-nightwatch/). If the gap is alerts on failed jobs and missed tasks, start with [Laravel queue monitoring](/guides/laravel-failed-jobs/). --- Source: https://lampwick.dev/integrations/ # Integrations: frameworks, alert channels and coding agents > Connect Lampwick to your stack: Laravel, Node.js, Next.js and Python, OpenTelemetry, a browser snippet, alerts in Slack, Discord or Teams, and MCP. Node.js, Next.js and Python connect over OpenTelemetry; Laravel has its own package. Each page has the setup snippet, a prompt for your coding agent and a link to the full docs. For any other language, [HTTP ingest](/integrations/http-ingest/) takes records as JSON, and the [API](/docs/api/) reads and changes what the dashboard shows. ## Frameworks and languages - [Laravel](/integrations/laravel/): Exceptions, requests, queries, jobs and scheduled tasks - [Node.js](/integrations/node/): Express, Fastify and more, via auto-instrumentation (via OpenTelemetry) - [Next.js](/integrations/nextjs/): Server requests and errors via OpenTelemetry (via OpenTelemetry) - [Python](/integrations/python/): FastAPI and Django via OpenTelemetry (via OpenTelemetry) - [HTTP ingest](/integrations/http-ingest/): POST JSON records from any language or script ## Via OpenTelemetry - [OpenTelemetry](/integrations/opentelemetry/): Traces and logs over OTLP/HTTP, from any SDK - [OpenTelemetry Collector](/integrations/opentelemetry/#collector): Add Lampwick as an otlphttp exporter ## Browser and no-code - [Browser errors](/integrations/browser/): One script tag: JavaScript errors become issues - [Lovable](/for/lovable/): Uptime and browser errors for Lovable apps - [Replit](/for/replit/): Uptime and browser errors for Replit apps - [v0](/for/v0/): Uptime and browser errors for v0 apps ## Alerts - [Email](/docs/alerts/): Alerts to every team member, on by default (Built in) - [Push notifications](/docs/alerts/): Alerts and new issues in the iOS app (Built in) - [Slack](/integrations/slack/): Alerts and new issues in a Slack channel - [Discord](/integrations/discord/): Alerts and new issues in a Discord channel - [Microsoft Teams](/integrations/microsoft-teams/): Alerts and new issues in a Teams channel - [Webhooks](/integrations/webhooks/): Signed JSON to your own endpoint ## AI agents - [MCP server](/integrations/mcp/): Agents read issues, logs and monitors, and act on them - [Claude Code](/docs/mcp/#claude-code): claude mcp add, then sign in with /mcp (Docs) - [Cursor](/docs/mcp/#cursor): Sign in, or a token in ~/.cursor/mcp.json (Docs) - [Codex](/docs/mcp/#codex-cli): A personal token in ~/.codex/config.toml (Docs) - [Claude](/integrations/claude-ai/): In Claude's connector directory: sign in, no token - [ChatGPT](/docs/mcp/#sign-in): A custom connector: sign in, no token (Docs) --- Source: https://lampwick.dev/integrations/browser/ # Catch JavaScript errors with one script tag > Add Lampwick's script tag to your site's head and errors in your visitors' browsers become issues, next to your server's. No package, no build step. A script tag in your site's `` reports JavaScript errors from your visitors' browsers to Lampwick. It needs no package and no build step, so it also works in apps made with Lovable, Replit or v0. ## What the browser script reports - Uncaught errors and unhandled promise rejections, with the stack trace, under **Issues** with a **Browser** label. - Optional: `console.error` calls (`data-console`) and failed `fetch()` calls (`data-fetch`). - Page URLs without the query string and fragment, which often hold tokens. - A monthly budget of its own for browser errors, on top of your plan's events. Lampwick doesn't apply source maps, so stack frames show positions in your bundle. ## Set it up On the project's **Settings** page, under **Browser script**, click **Get the snippet**. Copy the line into the `` of your site's `index.html`, above the other scripts: ```html ``` `YOUR_BROWSER_KEY` starts with `lwpub_`; the snippet on the **Settings** page has it filled in. The key is meant to be in your page: it can only send errors to this project. To test it, open your site and run this in the browser console: ```js setTimeout(() => { throw new Error('Lampwick test') }) ``` ## Let your app builder do it Paste this into the chat of Lovable, Replit or v0, with your key in it (the **Settings** page has this prompt ready to copy): ```text Add the Lampwick error monitoring snippet to the of index.html, exactly as written, before any other script: Don't change the key or move it to an environment variable: it is a public, write-only key that is meant to be in the page. Don't wrap it in a component or install a package for it. ``` ## Next step The [browser errors docs](/docs/browser/) cover the options, **Allowed sites**, the limits and troubleshooting. --- Source: https://lampwick.dev/integrations/claude-ai/ # Connect Lampwick to Claude > Connect Lampwick from Claude's connector directory, sign in and choose read-only or read & write. Works in claude.ai, Desktop, mobile and Claude Code. With Lampwick connected, Claude can read your issues, requests, logs and uptime monitors, and act on them if you allow it. You sign in to Lampwick once and pick the access; there is no token to copy. The connector works in claude.ai, Claude Desktop, the Claude mobile apps and Claude Code. You need a Lampwick account. On Claude's Team and Enterprise plans an Owner adds connectors for the organization (see [Team and Enterprise](#team-and-enterprise)). ## Connect from the directory 1. Open [Lampwick in Claude's connector directory](https://claude.ai/directory/lampwick), or search for Lampwick under **Customize** → **Connectors** → **Browse connectors**. 2. Click **Connect**. Lampwick opens in your browser: log in, with your two-factor code if you turned it on. 3. On **Connect … to Lampwick?**, choose **Read-only (recommended)** or **Read & write**, then click **Connect**. Pick **Read-only** unless you want Claude to change things: it is the safe default, and you can connect again with **Read & write** later. ## Or add it as a custom connector The same server, added by hand (for example in a Claude organization that only allows custom connectors): 1. Open **Customize** → **Connectors**. 2. Select **Add**, then **Add custom connector**. 3. Enter the name `Lampwick` and the URL `https://app.lampwick.dev/mcp`, then click **Continue**. 4. Keep the authentication settings Claude marks **Detected**. Leave the OAuth client ID and secret empty: Lampwick accepts Claude's published identity and automatic registration, so you don't create a client. Click **Add**. 5. Click **Connect**. Lampwick opens in your browser: log in, with your two-factor code if you turned it on. 6. On **Connect … to Lampwick?**, choose **Read-only (recommended)** or **Read & write**, then click **Connect**. Pick **Read-only** unless you want Claude to change things: it is the safe default, and you can connect again with **Read & write** later. Back in Claude, the connector shows **Connected** under **Your connectors**. Its page lists Lampwick's tools under **Tool permissions**. ### Team and Enterprise {#team-and-enterprise} An Owner adds the connector once for the organization: **Organization settings** → **Connectors** → **Add** → **Custom** (type **Web** if Claude asks), with the same name and URL. Members then find Lampwick under **Customize** → **Connectors** with the **Custom** label, click **Connect** and sign in with their own Lampwick account. Each member sees only the Lampwick teams they belong to. ## Claude Desktop and the mobile apps Claude Desktop and the Claude mobile apps use the connectors of your Claude account. Add Lampwick once as above, and it is there in each app. Sign-in goes through the same Lampwick consent screen. ## Claude Code Add the server without a token, then run `/mcp` in Claude Code and sign in from there: ```bash claude mcp add --transport http lampwick https://app.lampwick.dev/mcp ``` Claude Code opens Lampwick in your browser for the same sign-in and access choice. To use a personal token instead, see [the Claude Code token setup](/docs/mcp/#claude-code). ## Check that it works In a chat, click **+** → **Connectors** and turn Lampwick on. Then ask "Which Lampwick projects can you see?". Claude calls `listProjects` and lists the projects of your teams. Other things to ask: - "Which open issues in Acme Shop were first seen in the last 24 hours?" - "What are the 10 slowest routes by p95 this week?" - "Which uptime monitors are down right now?" ## Read-only or read & write - **Read-only**: Claude looks at issues, requests, logs, scheduled tasks and monitors. It can't change anything. - **Read & write**: Claude also resolves or ignores issues, adds and changes monitors, renames projects and deletes things. Never: delete a team, rotate a project's ingest token or change its browser key. Every tool that changes something is marked as destructive, except the ones that only add something (a project, a monitor, a deploy marker). Claude asks you before it runs a destructive tool. You can also block single tools on the connector's page under **Tool permissions**. What Claude reads includes error messages and logs from your apps, which could contain instructions meant for it. With read-only access, those can't change anything. ## Disconnect - In Lampwick: **Account & security** → **Connected clients** → **Disconnect**. Claude's access stops at once. - In Claude: **Customize** → **Connectors**, then **Remove** on Lampwick. A connection that goes unused for 60 days stops working. Resetting your Lampwick password or turning two-factor authentication on or off disconnects every connected client; click **Connect** again to sign in. ## Troubleshooting | What you see | Cause and fix | |---|---| | The connector shows **Connect** or **Reconnect** | Sign-in didn't finish, or the connection was disconnected. Click it and log in to Lampwick again. | | Claude says the connection is read-only | You chose **Read-only**. Disconnect in Lampwick, connect again and choose **Read & write**. | | A project is missing | Claude sees only the teams of the Lampwick account you signed in with. Ask an owner to invite you to the team. | | A tool fails with `403` on a read & write connection | That action is for team owners. | For other clients (ChatGPT, Cursor, VS Code, Codex) and for personal tokens, see [the MCP server docs](/docs/mcp/). --- Source: https://lampwick.dev/integrations/discord/ # Lampwick alerts in Discord > Send Lampwick alerts and new or reopened issues to a Discord channel through a channel webhook. Team owners add it under Alert channels and send a test message. An alert channel posts a team's alerts, and its new and reopened issues, to a Discord channel. Push notifications and email keep going out as well. ## Which alerts go to Discord - **Down** and **Up again** for uptime monitors, SSL and domain expiry, **DNS records changed**, **Broken links**, and **Missed**, **Failed** or **Slow** scheduled tasks. - New and reopened **Issues**, with the exception class; turn on **Include exception messages in issue notifications** for the message too. - What happened, the reason and a link to the dashboard, never a URL's query string. - For all projects of a team, or for one project. ## Set it up 1. In Discord, open the channel settings, go to **Integrations → Webhooks**, create a webhook and copy its URL. It looks like this: ```text https://discord.com/api/webhooks/… ``` 2. In Lampwick, click **Alert channels** in the sidebar. 3. Under **Add a channel**, pick **Discord** as the **Kind**, give it a **Name** and paste the **Webhook URL**. 4. Under **Alerts of**, pick **All projects** of a team or one project, and click **Add channel**. 5. On the channel's page, click **Send test message**. Only team owners add channels; members see them. The URL is stored encrypted and never shown again, only its host. ## Ask your agent When an alert lands in Discord, paste it into an agent that is [connected to Lampwick](/integrations/mcp/): ```text Lampwick just alerted in Discord: "Failed: the nightly backup task". Look at the last runs of that scheduled task and at the error logs around them, and tell me why it failed. ``` ## Next step The [alerts docs](/docs/alerts/#alert-channels) cover the hourly limits, retries and the **Deliveries** log of each channel. --- Source: https://lampwick.dev/integrations/http-ingest/ # Send records to Lampwick over HTTP > POST a JSON array of records to https://app.lampwick.dev/ingest with your project's ingest token, from any language, cron script or deploy hook. HTTP ingest is one endpoint that takes a batch of records from anything that can make an HTTPS request: a service in a language without a Lampwick package, a cron script, a deploy hook. The [Laravel package](/integrations/laravel/) sends to the same endpoint. ## What you can send over HTTP - Logs, exceptions, requests, jobs, scheduled tasks and the other record types, in the same pages as records from the Laravel package. - Exceptions grouped into **Issues** by the `_group` key you send. - Records tied to their request or job with `trace_id` or `execution_id`. ## Set it up ```bash curl -X POST https://app.lampwick.dev/ingest \ -H "Authorization: Bearer YOUR_PROJECT_TOKEN" \ -H "Content-Type: application/json" \ -d '[{"t":"log","level":"error","message":"Backup failed"}]' ``` `YOUR_PROJECT_TOKEN` is the project's ingest token, from the **Connect your app** card on its **Settings** page. Lampwick answers `202` with the number of records it took: ```json {"accepted": 1} ``` The log line shows up under **Logs** in the project. ## Let your agent do it ```text Send this script's failures to Lampwick over HTTP. Read https://lampwick.dev/docs/http-ingest.md, then POST a JSON array of records to https://app.lampwick.dev/ingest with the ingest token from the LAMPWICK_TOKEN environment variable as a bearer token. Keep the token out of git and the chat, and check that Lampwick answers 202. ``` ## Next step The [HTTP ingest docs](/docs/http-ingest/) list the record fields, every response code and the fixes for each. Running a framework with an OpenTelemetry SDK? [OpenTelemetry](/integrations/opentelemetry/) needs no record format at all. --- Source: https://lampwick.dev/integrations/laravel/ # Monitor a Laravel app with Lampwick > The lampwick/laravel package sends requests, queries, jobs, exceptions, logs and scheduled tasks from Laravel 12 or 13 to Lampwick. Three commands to set up. The `lampwick/laravel` package is Lampwick's own integration for Laravel. Your app hands its records to an ingest daemon on the same server, so requests never wait on the network. ## What Lampwick monitors in a Laravel app | What | What you see | Docs | |---|---|---| | Exceptions | **Issues**: grouped by class, file and line, with the request or job they happened in | [Issues](/docs/issues/) | | Slow requests | **Requests**: duration and p95 per route, each request with its queries and logs | [Laravel](/docs/laravel/) | | Queries | **Queries**: every query with its duration and the request or job that ran it | [Laravel](/docs/laravel/) | | Jobs | **Jobs** and **Queued jobs**: attempts, failures and durations | [Laravel](/docs/laravel/) | | Scheduled tasks | **Schedule health**: an alert when a task misses a run, fails or runs too long | [Schedule health](/docs/cron-health/) | | Logs | **Logs** from the `lampwick` log channel, next to the request that wrote them | [Laravel](/docs/laravel/#install-the-package) | | Uptime | **Uptime**: an alert after 2 failed checks in a row, and when it is up again | [Uptime](/docs/uptime/) | | SSL and domain | Alerts 14, 7, 3 and 1 days before a certificate expires, and 30 to 1 days before the domain does | [SSL, domain & DNS](/docs/host-checks/) | | Deploys | **Deploys**: record each deploy from CI, and every issue shows the deploy it was first seen in | [API](/docs/api/) | Requirements: PHP 8.2 or later with the `curl` extension, Laravel 12 or 13. ## What it costs Per team, excluding VAT. Every plan has all of the above; they differ in volume and history. | Plan | Monthly | Events / month | History | Uptime monitors | |---|---|---|---|---| | Free | €0 | 500k | 3 days | 3, every 5 min | | Pro | €29 | 5M | 14 days | 20, every minute | | Team | €79 | 25M | 30 days | 100, every 30 s | | Business | €199 | 100M | 60 days | 300, every 30 s | Yearly billing gets 2 months free. Projects, seats and the rest: [pricing](/pricing/). ## Set it up ```bash composer require lampwick/laravel ``` Add the project's ingest token, from the **Connect your app** card on the project's **Settings** page: ```dotenv LAMPWICK_TOKEN=YOUR_PROJECT_TOKEN ``` Start the ingest daemon, and check it from a second terminal: ```bash php artisan lampwick:agent ``` ```bash php artisan lampwick:test ``` Open your app: the request shows up in Lampwick within seconds. In production, run `lampwick:agent` under Forge, Supervisor or a sidecar container, like a queue worker. ## Let your agent do it Paste this into Claude Code, Cursor or Codex, in your app's repository: ```text Add Lampwick monitoring to this Laravel app. Read https://lampwick.dev/docs/laravel.md and follow it: install lampwick/laravel, put LAMPWICK_TOKEN in .env (never in git or the chat), run php artisan lampwick:test, and set up php artisan lampwick:agent under the process manager this app uses in production. ``` No Lampwick account yet? The [agent setup](/docs/agents/) lets your agent create a workspace and hand you a claim link. ## Next step The [Laravel docs](/docs/laravel/) cover the production setup per host, every configuration variable and troubleshooting. --- Source: https://lampwick.dev/integrations/mcp/ # Connect your AI agent to Lampwick over MCP > Lampwick's MCP server lets Claude Code, Cursor, Codex, claude.ai and ChatGPT read your issues, requests, logs and monitors, and act on them if you allow it. The MCP server at `https://app.lampwick.dev/mcp` gives an AI agent the same data you see in the dashboard. Every tool is an [API](/docs/api/) operation, with the access you choose when you connect: **Read-only (recommended)** or **Read & write**. ## What an agent can read and change - Issues, requests, queries, logs, uptime monitors and scheduled tasks, as tools named after their API operation (`listIssues`, `getIssue`, …). - With **Read & write**: resolve issues and change monitors and projects. Never: delete a team, rotate an ingest token or change a browser key. - Sign-in without a token in claude.ai, Claude Desktop, ChatGPT, Cursor, VS Code and Claude Code; a personal `lwp_…` token for other clients. ## Set it up In Claude Code, add the server, then run `/mcp` and sign in from there: ```bash claude mcp add --transport http lampwick https://app.lampwick.dev/mcp ``` In claude.ai, ChatGPT, Cursor or VS Code, add a custom connector with the URL `https://app.lampwick.dev/mcp` and leave any client ID and secret fields empty. Lampwick opens in your browser: log in, choose the access, and click **Connect**. Ask "Which Lampwick projects can you see?" to check it works. ## Ask your agent Once it is connected, paste this in your app's repository: ```text Look at the open Lampwick issues of this project that were first seen since the last deploy. Pick the one with the most events, find its cause in this repository and propose a fix. Don't resolve the issue until I have reviewed the fix. ``` What an agent reads includes error messages and logs from your apps, which could contain instructions meant for it. With read-only access, those can't change anything. ## Next step The [MCP docs](/docs/mcp/) have the token setup for Claude Code, Codex, Cursor, VS Code, Gemini CLI, Zed, Cline and the Claude and OpenAI APIs. No account yet? Your agent can [set up Lampwick for you](/docs/agents/). --- Source: https://lampwick.dev/integrations/microsoft-teams/ # Lampwick alerts in Microsoft Teams > Send Lampwick alerts and new or reopened issues to a Microsoft Teams channel through a Workflows webhook. Team owners add it under Alert channels. An alert channel posts a team's alerts, and its new and reopened issues, to a Microsoft Teams channel. Push notifications and email keep going out as well. ## Which alerts go to Microsoft Teams - **Down** and **Up again** for uptime monitors, SSL and domain expiry, **DNS records changed**, **Broken links**, and **Missed**, **Failed** or **Slow** scheduled tasks. - New and reopened **Issues**, with the exception class; turn on **Include exception messages in issue notifications** for the message too. - What happened, the reason and a link to the dashboard, never a URL's query string. - For all projects of a team, or for one project. ## Set it up 1. In Microsoft Teams, open **Workflows** and create a workflow from the template "Post to a channel when a webhook request is received". Pick the channel and copy the `https://` URL it gives you. 2. In Lampwick, click **Alert channels** in the sidebar. 3. Under **Add a channel**, pick **Microsoft Teams** as the **Kind**, give it a **Name** and paste the **Webhook URL**. 4. Under **Alerts of**, pick **All projects** of a team or one project, and click **Add channel**. 5. On the channel's page, click **Send test message**. Only team owners add channels; members see them. The URL is stored encrypted and never shown again, only its host. ## Ask your agent When an alert lands in Teams, paste it into an agent that is [connected to Lampwick](/integrations/mcp/): ```text Lampwick just alerted in Teams: "SSL certificate expires in 7 days" for shop.example.com. Check that monitor's certificate details in Lampwick and tell me which certificate to renew and where it is served. ``` ## Next step The [alerts docs](/docs/alerts/#alert-channels) cover the hourly limits, retries and the **Deliveries** log of each channel. --- Source: https://lampwick.dev/integrations/nextjs/ # Monitor a Next.js app with Lampwick > Add instrumentation.ts with @vercel/otel and point it at Lampwick. Next.js App Router requests, queries and server errors show up, over OpenTelemetry. Next.js has OpenTelemetry built in. Add an `instrumentation.ts` that calls `@vercel/otel`, set 4 environment variables, and every server request and server-side error goes to Lampwick. We tested it with Next.js 16 (App Router, `next start`). ## What Lampwick records from a Next.js app - **Requests**, grouped by route, such as `GET /orders/[id]`. - **Queries**, with the `pg` instrumentation below. - **Issues** from server-side errors, tied to the request they happened in. ## Set it up ```bash npm install @vercel/otel @opentelemetry/api @opentelemetry/api-logs @opentelemetry/instrumentation @opentelemetry/sdk-logs ``` Create `instrumentation.ts` in the project root (or in `src/`): ```ts import { registerOTel } from '@vercel/otel'; export function register() { registerOTel({ serviceName: 'my-app' }); } ``` Set these in `.env.local`, or on Vercel under **Settings → Environment Variables**: ```dotenv OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN OTEL_METRICS_EXPORTER=none ``` `YOUR_PROJECT_TOKEN` is the project's ingest token, from the **Connect your app** card on its **Settings** page. For database queries, add `PgInstrumentation` (version 0.76.0); the [Next.js docs](/docs/nextjs/#database-queries) show how. ## What it doesn't do - **Stack traces point into `.next/server/chunks/`.** Next.js bundles server code and Lampwick doesn't apply source maps. The message and the request are exact. - **Logs aren't sent.** `console.log` isn't captured and `@vercel/otel` exports no logs. - **Browser errors** need the [browser snippet](/integrations/browser/) as well. - **Edge runtime**: we tested the Node.js runtime only. ## Let your agent do it ```text Add OpenTelemetry to this Next.js app so it reports to Lampwick: follow https://lampwick.dev/docs/nextjs.md exactly (instrumentation.ts with @vercel/otel, the environment variables, PgInstrumentation only if the app uses pg). Read the ingest token from the environment, never commit it. ``` ## Next step The [Next.js docs](/docs/nextjs/) have the app we tested, what showed up and the troubleshooting table. How spans map to Lampwick: [OpenTelemetry](/docs/opentelemetry/#what-shows-up-where). --- Source: https://lampwick.dev/integrations/node/ # Monitor a Node.js app with Lampwick > Send requests, queries, errors and pino logs from a Node.js app to Lampwick with the OpenTelemetry auto-instrumentations. No code changes. Node.js apps send to Lampwick through OpenTelemetry. The auto-instrumentations patch `http`, Express, `pg` and more when Node starts, so your code stays as it is. We tested it with Express 5, `pg` and pino on Node.js 26. ## What Lampwick records from a Node.js app - **Requests**, grouped by route, such as `GET /orders/:id`. - **Queries**, linked to the request they ran in. - **Issues** from unhandled errors, with the stack trace, counted once per request. - **Logs** from pino, winston or bunyan, linked to their request. ## Set it up ```bash npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node ``` Set these in the app's environment: ```bash export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN export OTEL_SERVICE_NAME=my-app export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=otlp export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" ``` `YOUR_PROJECT_TOKEN` is the project's ingest token, from the **Connect your app** card on its **Settings** page. `NODE_OPTIONS` loads the instrumentation before your app's code. Start the app as usual; requests show up within about 5 seconds. ## What it doesn't do - **`console.log` isn't sent.** Only pino, winston and bunyan are captured. - **Errors you catch yourself** become issues only when you record them on the span: `trace.getActiveSpan()?.recordException(err)`. - **Bundled code**: source maps aren't applied, so stack traces name the files Node runs. ## Let your agent do it ```text Add OpenTelemetry to this Node.js app so it reports to Lampwick: follow https://lampwick.dev/docs/node.md exactly (zero-code setup, environment variables, no code changes besides a pino logger if the app has no logger). Read the ingest token from the environment, never commit it. ``` ## Next step The [Node.js docs](/docs/node/) have the app we tested, what showed up and the troubleshooting table. How spans map to Lampwick: [OpenTelemetry](/docs/opentelemetry/#what-shows-up-where). --- Source: https://lampwick.dev/integrations/opentelemetry/ # Send OpenTelemetry traces and logs to Lampwick > Point any OpenTelemetry SDK or Collector at https://app.lampwick.dev over OTLP/HTTP with your project's ingest token. Spans become requests, jobs and issues. Lampwick takes traces and logs over OTLP/HTTP, so an app in any language with an OpenTelemetry SDK can send to it without a Lampwick package. We tested [Node.js](/integrations/node/), [Next.js](/integrations/nextjs/) and [Python](/integrations/python/) end to end. Lampwick turns spans and log records into its own records, following the semantic conventions. ## Where spans and logs show up in Lampwick | OpenTelemetry | In Lampwick | |---|---| | SERVER spans | **Requests**, grouped by `http.route` | | Spans with `db.system` | **Queries** | | HTTP client spans | **Outgoing requests** | | CONSUMER and PRODUCER spans | **Jobs** and **Queued jobs** | | `exception` span events | **Issues**, grouped by type and your own top stack frames | | Log records | **Logs**, tied to their request or job | | `enduser.id`, `user.id` | **Users** | Queries, logs and exceptions show up under their request or job, through the span hierarchy. Other spans (middleware, router layers) are not stored and don't count as events. gRPC and metrics are not supported: use `http/protobuf` or JSON, and turn the metrics exporter off. ## Set it up Set these environment variables for your app. The SDK adds `/v1/traces` and `/v1/logs` to the endpoint: ```dotenv OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN OTEL_TRACES_EXPORTER=otlp OTEL_LOGS_EXPORTER=otlp OTEL_METRICS_EXPORTER=none OTEL_SERVICE_NAME=shop-api ``` `YOUR_PROJECT_TOKEN` is the project's ingest token, from the **Connect your app** card on its **Settings** page. `OTEL_EXPORTER_OTLP_PROTOCOL` matters for Python and .NET, which default to gRPC. Set `service.version` in `OTEL_RESOURCE_ATTRIBUTES` and it shows as the deploy. ### Through an OpenTelemetry Collector {#collector} Already run a Collector? Add Lampwick as an exporter: ```yaml exporters: otlphttp/lampwick: endpoint: https://app.lampwick.dev headers: x-lampwick-token: ${env:LAMPWICK_TOKEN} service: pipelines: traces: { receivers: [otlp], processors: [batch], exporters: [otlphttp/lampwick] } logs: { receivers: [otlp], processors: [batch], exporters: [otlphttp/lampwick] } ``` ## Let your agent do it ```text Send this app's OpenTelemetry traces and logs to Lampwick. Use OTLP over HTTP (http/protobuf) to https://app.lampwick.dev with the header x-lampwick-token set from the LAMPWICK_TOKEN environment variable, turn the metrics exporter off, and set OTEL_SERVICE_NAME. Keep the token out of git and the chat. Then make one request to the app and tell me which spans it exported. ``` ## Next step The [OpenTelemetry docs](/docs/opentelemetry/) have the full mapping, the limits and troubleshooting. Tested setups per language: [Node.js](/docs/node/), [Next.js](/docs/nextjs/) and [Python](/docs/python/). Each stored record counts as one event, as with [HTTP ingest](/docs/http-ingest/); the [free plan](/pricing/) has 500k events a month. --- Source: https://lampwick.dev/integrations/python/ # Monitor a Python app with Lampwick > Start FastAPI or Django with opentelemetry-instrument and Lampwick gets requests, queries, errors and logging records. No code changes. Python apps send to Lampwick through OpenTelemetry. You start the app with `opentelemetry-instrument` and set environment variables; the code stays as it is. We tested FastAPI on uvicorn and Django with `runserver`. ## What Lampwick records from a Python app - **Requests**, named after the route or URL pattern, such as `GET /orders/{order_id}`. - **Queries**, linked to the request they ran in. - **Issues** from unhandled exceptions with the stack trace, and handled ones from `logger.exception`. - **Logs** from `logging`, linked to their request. ## Set it up In the app's virtualenv: ```bash pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http opentelemetry-bootstrap -a install ``` `opentelemetry-bootstrap` adds the instrumentations for what's installed. Run it again after adding a framework or database driver. Then set: ```bash export OTEL_EXPORTER_OTLP_ENDPOINT=https://app.lampwick.dev export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export OTEL_EXPORTER_OTLP_HEADERS=x-lampwick-token=YOUR_PROJECT_TOKEN export OTEL_SERVICE_NAME=my-app export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=otlp ``` `YOUR_PROJECT_TOKEN` is the project's ingest token, from the **Connect your app** card on its **Settings** page. Python defaults to gRPC, which Lampwick doesn't accept, so keep `OTEL_EXPORTER_OTLP_PROTOCOL`. Start the app through the instrumentation: ```bash opentelemetry-instrument uvicorn main:app --port 8000 ``` For Django, set `DJANGO_SETTINGS_MODULE` and turn the autoreloader off: ```bash export DJANGO_SETTINGS_MODULE=mysite.settings opentelemetry-instrument python manage.py runserver --noreload ``` ## What it doesn't do - **`connection.execute()` on `sqlite3` isn't traced**, only `cursor.execute()`. Django and SQLAlchemy use cursors. - **Info logs need a level**: Python drops records below `WARNING` unless you configure logging. - **gunicorn and uWSGI** fork workers after the SDK starts. We haven't tested the `post_fork` hook they need. ## Let your agent do it ```text Add OpenTelemetry to this Python app so it reports to Lampwick: follow https://lampwick.dev/docs/python.md exactly (opentelemetry-distro, opentelemetry-bootstrap, opentelemetry-instrument, the environment variables). Read the ingest token from the environment, never commit it. ``` ## Next step The [Python docs](/docs/python/) have the apps we tested, what showed up and the troubleshooting table. How spans map to Lampwick: [OpenTelemetry](/docs/opentelemetry/#what-shows-up-where). --- Source: https://lampwick.dev/integrations/slack/ # Lampwick alerts in Slack > Send Lampwick alerts and new or reopened issues to a Slack channel through an incoming webhook. Team owners add it under Alert channels and send a test message. An alert channel posts a team's alerts, and its new and reopened issues, to a Slack channel. Push notifications and email keep going out as well. ## Which alerts go to Slack - **Down** and **Up again** for uptime monitors, SSL and domain expiry, **DNS records changed**, **Broken links**, and **Missed**, **Failed** or **Slow** scheduled tasks. - New and reopened **Issues**, with the exception class; turn on **Include exception messages in issue notifications** for the message too. - What happened, the reason and a link to the dashboard, never a URL's query string. - For all projects of a team, or for one project. ## Set it up 1. In Slack, go to **Apps → Incoming Webhooks**, pick the channel and copy the webhook URL. It looks like this: ```text https://hooks.slack.com/services/… ``` 2. In Lampwick, click **Alert channels** in the sidebar. 3. Under **Add a channel**, pick **Slack** as the **Kind**, give it a **Name** (such as `#ops`) and paste the **Webhook URL**. 4. Under **Alerts of**, pick **All projects** of a team or one project, and click **Add channel**. 5. On the channel's page, click **Send test message**. Only team owners add channels; members see them. The URL is stored encrypted and never shown again, only its host. ## Ask your agent When an alert lands in Slack, paste it into an agent that is [connected to Lampwick](/integrations/mcp/): ```text Lampwick just alerted in Slack: "Down: Checkout API, HTTP 404 Not Found, 2 failed checks in a row". Look at that monitor and at the issues and error logs of the same project around that time, and tell me the most likely cause. ``` ## Next step The [alerts docs](/docs/alerts/#alert-channels) cover the hourly limits, retries and the **Deliveries** log of each channel. --- Source: https://lampwick.dev/integrations/webhooks/ # Lampwick alerts as signed webhooks > Lampwick POSTs alerts and new or reopened issues as signed JSON to your own HTTPS endpoint, with an HMAC-SHA256 signature you check with the channel's secret. A webhook channel sends a team's alerts, and its new and reopened issues, to your own `https://` endpoint as a `POST` with a JSON body. Use it to open a ticket, page someone or start a script. ## What each webhook request contains - One request per alert or issue, with `event` (such as `monitor_down`, `new_issue`, `regression` or `test`), `title`, `body`, a dashboard `url`, and the project and team. - `X-Lampwick-Signature`: `v1=` and the hex HMAC-SHA256 of `TIMESTAMP.BODY`, keyed with the channel's signing secret (`whsec_…`). - `X-Lampwick-Delivery`, the same on retries, so you can drop duplicates. - Retries on a network error, timeout, `408`, `429` or `5xx`: 4 times, over about 8 minutes. ## Set it up 1. Click **Alert channels** in the sidebar. 2. Under **Add a channel**, pick **Webhook** as the **Kind**, give it a **Name** and paste your endpoint's `https://` URL (port 443 or 8443). 3. Under **Alerts of**, pick **All projects** of a team or one project, and click **Add channel**. 4. Copy the signing secret from the channel's page, then click **Send test message**. Check the signature before you trust a request. In Python: ```python import hashlib import hmac import time def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool: if abs(time.time() - int(timestamp)) > 300: return False expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` Pass the raw request body, and the `X-Lampwick-Timestamp` and `X-Lampwick-Signature` headers. It refuses requests older than 5 minutes and compares in constant time. ## Let your agent do it ```text Add an endpoint to this app that receives Lampwick alert webhooks. Read the "Webhook requests" section of https://lampwick.dev/docs/alerts.md. Verify X-Lampwick-Signature (v1= hex HMAC-SHA256 of "TIMESTAMP.BODY" with the secret from the LAMPWICK_WEBHOOK_SECRET environment variable) over the raw body in constant time, refuse timestamps older than 5 minutes, drop repeated X-Lampwick-Delivery ids, and answer 2xx quickly. ``` ## Next step The [webhook requests docs](/docs/alerts/#webhook-requests) show every header and a full example body. --- Source: https://lampwick.dev/legal/dpa/ # Data processing agreement > The GDPR article 28 agreement for the monitoring data you send to Lampwick, with the security measures and the list of sub-processors. This data processing agreement ("DPA") is part of the [Terms of service](/legal/terms/) between you, the controller, and NIO Media, the processor. NIO Media is the trade name of Niels Oosterhoff, sole proprietorship (eenmanszaak) registered with the Netherlands Chamber of Commerce under number 69855773, Roomweg 68, 7523 BR Enschede, the Netherlands. Lampwick is a service of NIO Media; "we" and "Lampwick" mean NIO Media. The DPA applies automatically when you use Lampwick to process personal data; you don't need to sign anything. If this DPA and the terms conflict about personal data, this DPA wins. ## Subject and duration Lampwick processes personal data contained in the data your apps, scripts and team members send to or create in Lampwick ("Customer Data", or "your data"), to provide the monitoring service described in the terms. Processing lasts as long as you use the service, and ends as described in [Deletion at the end](#deletion-at-the-end). ### Nature and purpose - Receiving, storing, grouping, analyzing and showing monitoring data. - Product analytics on how your app's signed-in users use it: active users, retention per day, week or month, the impact of an error on the users it hits, funnels, user paths and feature usage. These come from their requests and the custom events your server sends. - Anonymous counts of the custom events and page views the browser script sends. - CSV exports of these reports. - Sending alerts about your data, checking the URLs and hosts you configure, and publishing the status pages you choose to publish. ### Categories of data What you send decides what we hold. Depending on your integration, your data can include: - **Requests, jobs, commands and queries**: URLs and routes, HTTP methods and status codes, the client IP address, request headers (Lampwick removes `Authorization`, `Cookie` and other credential headers on arrival), request bodies of failed requests if you turn that on, SQL text, cache keys, mail subjects, timings. - **Errors and logs**: exception classes and messages, stack traces, source lines around the error, log messages and their context. - **Users of your app**: - the user id, name and username or email address your integration reports, with their activity and errors; - per user and month, the days they were active (a request, an exception, or a custom event or page view your server sent with their id), with counts of their requests and exceptions, for active users, retention and the impact of errors; - which steps of the funnels you define they reached. You can erase one user's data (see [Erasing an app user](#erasing-an-app-user)), and choose to store client IP addresses shortened (IPv4 to /24, IPv6 to /48). - **Custom events and page views from your server**: the event name with up to 20 properties (strings, numbers or booleans; query strings are removed from URLs in them), or the path of a page view, with id-like segments replaced by `{id}` and without query string or fragment. With it: the time, the release, where it came from (its source), the trace or execution it happened in, and the app user id if your server attaches one. Your server sends them over HTTP or TCP ingest, or as OpenTelemetry log records with an event name. Lampwick rebuilds each one on arrival from these fields and drops the rest, so no full URL, page title, IP address or user agent is stored with them. - **Browser analytics**: the custom events (`lampwick.track`) and page views (`data-pages` or `lampwick.page`) the browser script sends, with the same fields. They are anonymous counts. Lampwick stores them without a user, also when the page sets one with `lampwick.setUser`, and never links them to an app user, a device or a visitor. It also drops properties named like an identifier (such as `user_id`, `email`, `session` or `ip`) and property values that hold an email address. - **Browser errors** (the browser script): the page URL without query string or fragment, the browser's user agent, error messages and stack traces, failed `fetch` targets if turned on, and the user id, name and email address if you call `lampwick.setUser`. That user id comes from the page and isn't verified: anyone with your public browser key can send errors under a user id. A key may bring in at most 2,000 new user ids a day; past that, errors are stored without their user. Limit the key to your own sites on the project's **Settings** page. Browser records never rename a user your server already sent. The visitor's IP address is not stored with the error; it is used in memory for rate limiting and appears in our web server access logs, like every request. - **OpenTelemetry** traces and logs, including `enduser.id` and `user.id`. A log record with an event name becomes a custom event. Its attributes become the properties, except user, session, client, server, source, network, address and header attributes. - **Deploys**: version, commit, environment and author. - **Uptime, host checks and site crawls**: the URLs you monitor, response times and status codes, certificate, domain and DNS data, and links found on your pages. - **Status pages**: the names and notices you publish. Visitors' IP addresses are used in memory for rate limiting and appear in our web server access logs. Our web server access logs (IP address, time, URL, user agent) cover every request, including ingest, browser errors and status pages, and are kept for up to 14 days. ### Data subjects Users and visitors of your apps and websites (including the signed-in users whose app user id your server attaches to requests and custom events), visitors of your status pages, your staff and contractors, and anyone else whose data appears in your logs or errors. ### Special categories Lampwick is not designed for special categories of personal data (art. 9 GDPR), data about criminal convictions (art. 10), or payment card data. You must not send them. Event names, event properties and page paths can reveal them too, for example `therapy.booked` or `/clinic/oncology`: don't send those either. ## Your instructions and obligations Lampwick processes your data only on your documented instructions, including with regard to transfers to a third country. The terms, this DPA and the settings you choose in the app (what to collect, retention through your plan, alert channels, status pages, deletion) are your instructions. If we think an instruction breaks the GDPR, we tell you. If EU or Dutch law requires us to process data otherwise, we tell you first, unless that law forbids it. You are responsible for: - having a legal basis for sending the data, including for product analytics on your users, and informing the people it is about, for example in your privacy policy; - sending only what you need. Use the redaction settings of the integrations (the Laravel package redacts passwords, tokens and similar fields by default, and doesn't send request bodies unless you turn that on). Keep personal data you don't need out of log messages, exception messages, event names, event properties and page paths: the app user id is the only identifier the product analytics need; - page paths in particular: Lampwick replaces id-like path segments, but readable ones (such as `/u/ada-lovelace`) stay. Send the route instead (`lampwick.page('/u/:name')`); - keeping identifiers (user ids, session ids, email addresses, IP addresses) out of the properties of browser events, so they stay anonymous. Lampwick drops the obvious ones, but can't recognize every identifier; - not sending special categories of personal data; - the settings you choose, such as turning on request payloads or exception messages in alert channels, and what your team does with the exports it downloads. ### Analytics and your users' devices Lampwick is built so that its analytics don't need your users' consent under the ePrivacy rules on storing or reading information on a device (art. 5(3) of the ePrivacy Directive; in the Netherlands art. 11.7a Telecommunicatiewet). Our position: - **Server-side analytics** don't access your users' devices. Funnels, user paths, retention and active users come only from your server's requests and the custom events your server sends. - **Browser analytics** are anonymous aggregate statistics. The browser script stores nothing on the device: no cookies, no localStorage. Lampwick doesn't link these counts to a person, a device or a visitor, doesn't share them with third parties and doesn't link them across sites. We consider them covered by the Dutch exemption for analytics with little or no impact on privacy (art. 11.7a(3) Tw). - **Browser error reporting** is needed to keep your app working for the people who use it. We consider it strictly necessary for the service they asked for, which the same article exempts. Whether your own app needs consent for other reasons, such as other scripts or cookies, stays your decision as controller. ### If you are a processor yourself If you process Customer Data as a processor for your own client (for example as an agency monitoring a client's app), Lampwick acts as your sub-processor; you confirm that your controller has authorized this, and you pass on to us its instructions. We deal only with you. ## Our obligations Lampwick will: - process your data only to provide the service and as instructed, never for its own purposes such as advertising, AI training or selling it; - make sure everyone with access is bound to confidentiality; - take the measures in [Security measures](#security-measures); - help you, as far as we reasonably can, to answer requests from data subjects, and with data protection impact assessments and prior consultations (art. 32 to 36 GDPR), for example for the per-user funnels, user paths and retention; - not link your users across customers or sites; - tell you about a personal data breach as described in [Breach notification](#breach-notification); - make the information you need to show compliance available, and allow audits as described in [Audits](#audits). We forward any request we receive directly from one of your data subjects to you and don't answer it ourselves, unless you ask us to. ### Erasing an app user You can erase one of your app's users on their page in Lampwick (**Users** → the user → **Erase this user's data**) or with the API (`deleteAppUser`). This deletes their name and username, their activity (active days and counts, and the hourly activity it is built from) and their funnel attempts, and removes from each of their records the user id and the fields about them: the client IP address, request headers, request bodies and the properties of their custom events. The records themselves stay, without a link to the user, until your plan's retention removes them, so counts and charts don't change. That includes the names of their custom events and the paths of their page views, and those names and paths in the event and page lists. Daily totals without user ids (funnel counts, user paths, users per route, event or page) stay too. The audit log records the erasure with a keyed hash of the user id, not the id itself. Text your app put elsewhere (exception messages, log lines, URLs) isn't searched. Data your app sends about the user afterwards is stored again. Erased data leaves the backups within about 30 days. Browser analytics hold no user, so there is nothing of theirs to erase there. Our assistance under this DPA is free as far as the self-service features cover it. For other assistance we may charge reasonable costs at our usual rates, agreed in advance, unless the request results from our breach of this DPA or the GDPR. ## Customer-directed transfers Some features send your data where you tell them to. These recipients are your choice, not our sub-processors, and you are responsible for that transfer: - **Alert channels**: Slack, Discord, Microsoft Teams or your own webhook endpoint. Messages can contain exception messages; you can turn that off per channel. - **API and MCP clients**: tokens and connected apps (for example an AI assistant connected through `/mcp`) read the data the token's account can see. - **Public status pages**: what you publish is visible to anyone. - **Exports**: CSV downloads and API responses your team members request, such as the users list (ids, names and usernames), funnels, retention and route usage. What happens to a downloaded file is up to you. ## Sub-processors You give general authorization for the sub-processors below. We impose on each sub-processor, by contract, the same data protection obligations as in this DPA, and we remain responsible to you for its performance of those obligations (art. 28(4) GDPR), subject to the liability section of the Terms. | Sub-processor | Purpose | Your data involved | Location | |---|---|---|---| | Hetzner Online GmbH | Servers, database and backups | All of it | Germany | | Lettermint | Sending alert and service emails | Recipient addresses and alert content: monitor names, failure reasons, URL host and path; for funnel alerts the funnel name, the step labels (routes, event names, page paths) and their conversion | EU (the Netherlands) | | Apple Inc. (Apple Push Notification service) | Push notifications to the Lampwick iOS app, only for team members who install it | Notification text. Issues: project name, issue number, error class and message (for browser errors, the class only). Alerts: title and body, such as monitor names, failure reasons and URL host and path, or a funnel's name and step labels | US (EU-US Data Privacy Framework) | These parties are not sub-processors for your data: - **Stripe Payments Europe, Limited** processes billing data about you as our customer, partly as an independent controller. It receives no monitoring data. - **GitHub and Google** are used only when a person chooses to log in with them. GitHub also hosts our source code and runs our deployments (GitHub Actions), without customer data. - **Public DNS resolvers** (Cloudflare, Inc. and Google LLC, US companies) and **domain registries** (over RDAP) receive only the host and domain names of the URLs you monitor, to run DNS and domain expiry checks. ### Changes to sub-processors We announce a new or replaced sub-processor to team owners by email at least 30 days in advance, and on this page. You can object on reasonable data protection grounds within that period. If we can't resolve the objection, you can end the affected subscription before the change and get a pro-rata refund of prepaid fees. ## International transfers Your data is stored in the EU. It leaves the EU only through Apple's push service, for team members who use the iOS app (under the EU-US Data Privacy Framework), and through the customer-directed transfers above. If a sub-processor transfers your data and the Data Privacy Framework doesn't cover it, we use the Standard Contractual Clauses. ## Security measures These are the technical and organizational measures (art. 32 GDPR) in place today: - **Encryption in transit**: all traffic to lampwick.dev uses TLS (certificates from Let's Encrypt), with HSTS. Customers send data over HTTPS only; the unencrypted TCP ingest port is not reachable from the internet. - **Secrets at rest**: two-factor secrets and alert channel webhook URLs and signing secrets are encrypted with XChaCha20-Poly1305. Passwords are hashed with Argon2. Session ids, API tokens, OAuth tokens and invitation, verification and reset links are stored only as hashes. Project ingest tokens and browser keys are stored readable, because owners need to see them. - **Tenant isolation**: every project belongs to a team, and every page, API call, search and live stream is checked against the caller's team membership. A project of another team answers exactly like one that doesn't exist. - **Access control**: team roles (owner and member); only owners see ingest tokens, IP addresses in the audit log and alert channel URLs and secrets. Read-only API tokens. Optional two-factor login for every account. - **Abuse protection**: rate limits per IP on login, signup and other flows, temporary account lockout after repeated wrong passwords or codes, limits on ingest and public endpoints. - **Data minimization at ingest**: credential headers are removed on arrival; the browser snippet strips query strings, fragments and credentials from URLs before sending, and the server strips them again. Custom events and page views are rebuilt from a fixed set of fields: id-like path segments become `{id}`, properties are capped at 20 scalar values, and no IP address or user agent is stored with them. Those from the browser script are stored without a user. - **Browser key limits**: the public browser key can only write, and only from your sites if you list them. It is capped on custom events and page views per minute and per day, and on new event names, page paths and (on errors and logs) user ids per day. - **Exports**: at most 20 CSV exports per account per minute; text fields a spreadsheet would run as a formula are escaped. - **Audit log**: team and account changes are recorded with actor, IP and channel, kept 365 days. - **Infrastructure**: servers at Hetzner in Germany, firewall with only the needed ports open, SSH with keys only, automatic security updates, the database reachable only from the server itself. - **Availability and recovery**: nightly database backups, kept 7 days locally and about 30 days off-site in a different EU location, plus Hetzner server backups. - **Supply chain and deployment**: the source code is on GitHub. GitHub Actions builds each release and deploys it with a dedicated deploy key that can only upload a release and activate it. Releases ship only after automated tests pass, and roll back automatically when a release doesn't start. - **People**: only NIO Media's owner has access to production. We may change these measures as long as the overall level of security doesn't go down. ## Breach notification If we become aware of a personal data breach affecting your data, we notify the team owners by email without undue delay, and within 48 hours where we can. We tell you what we know: what happened, which data and how many people are likely affected, the likely consequences, and what we have done and will do. We add details as we learn them. Notifying the supervisory authority and the people affected is your decision as controller; we help you with it. ## Audits On request, we give you the information you need to show that we meet art. 28 GDPR, such as an up-to-date description of these measures. If that isn't enough, you or an independent auditor you mandate, bound to confidentiality, may audit our compliance: - at most once per 12 months, plus after a personal data breach affecting your data, or when a supervisory authority requires it; - with 30 days' written notice, during business hours, without disrupting the service or seeing other customers' data; - each party pays its own costs, unless the audit finds a material breach of this DPA by us: then we pay the reasonable cost of the audit. ## Retention Your data is kept as long as your plan's retention, after which it is deleted automatically. **Follows the plan's retention** (Free 3 days, Pro 14, Team 30, Business 60): - events: requests, errors, logs, queries, jobs and other records; - app users, counted from when they were last seen; - custom events and page views (stored as records, like the others); - each app user's activity per hour, from which the activity below is built; - the funnel steps each app user reached; and, counted per day without user ids, where users went next (user paths), the users per route, event or page (feature usage) and the funnels' daily totals; - uptime check results. **Activity of your app's users** (which days each user was active, with request and exception counts per month): Free 30 days, Pro 90 days, Team and Business 13 months. It holds no request, event or error details, so it is kept longer than events. Active users, retention and issue impact are built from it. **Fixed periods:** | Data | Kept | |---|---| | Issues (the grouped error: class, message, file and line) | 90 days after the last occurrence | | Host check history, crawl summaries, daily uptime for status pages | Up to the longest plan retention (60 days) | | Details of the latest successful crawl | Until the next successful crawl | | Alerts (including event alert notifications) | 30 days | | Property values an event alert shows, for notifications still to be sent | Until sent, then 2 days; cleared for the project when an app user is erased | | Alert channel delivery log | The latest 50 deliveries per channel | | Scheduled tasks that stopped running | Forgotten after 7 days | | Status page slug of a deleted or renamed page | Reserved for the team for 30 days | | New user ids, event names and page paths per browser key per day (for the caps) | Up to 2 days | **Kept until you delete them** (they don't follow the plan's retention): - deploys, up to the latest 1,000 per project per source; - monitors, status pages and their notices, alert channels and project settings. After a downgrade or cancellation, records keep the retention of the previous plan for 30 more days, and team owners get an email before that ends. Deleting a project or team deletes its data right away. ## Deletion at the end At the end of the agreement, you choose whether we return or delete your data: - **Return**: export it through the [API](/docs/api/) (the product analytics reports also as CSV), or ask us for a copy in JSON, during the retrieval period in the terms' [Switching and export](/legal/terms/#switching-and-export) section. - **Delete**: delete the project, team or account in the app, or ask us to. A team with a running subscription can be deleted once the paid period has ended. If you make no choice, we delete your data after the retrieval period. Deleted data leaves the live database at once and the backups within about 30 days. We keep no copies unless EU or Dutch law requires us to. ## Liability The liability limits of the [Terms of service](/legal/terms/#liability) apply to claims between you and us under this DPA, including recourse claims under art. 82(5) GDPR. They don't limit the rights of data subjects. Each party bears the administrative fines imposed on it. ## Changes to this DPA We change this DPA the same way as the terms: see [Changes to these terms](/legal/terms/#changes-to-these-terms). Changes to the sub-processors follow [Changes to sub-processors](#changes-to-sub-processors). ## Contact Questions about this DPA or about your data: [info@niomedia.nl](mailto:info@niomedia.nl). --- Source: https://lampwick.dev/legal/privacy/ # Privacy policy > What personal data Lampwick collects about its customers and website visitors, why, on what legal basis, how long we keep it, and your rights under the GDPR. Lampwick is a service of NIO Media. This policy explains what personal data we process when you visit lampwick.dev, create an account, are invited to a team, pay for a plan, use the iOS app or view a public status page. Lampwick has two roles under the GDPR: - **Controller** for the data about you as our customer or visitor: your account, your team membership, billing, security logs and the website. This policy covers that data. - **Processor** for the monitoring data your apps send us (errors, logs, requests, browser errors, custom events and page views, app users and the product analytics built from them; the browser script's events and page views are anonymous counts). That data often contains personal data of your own users. You decide what you send and why; we process it only on your instructions, under our [Data processing agreement](/legal/dpa/). If you are an end user of an app that uses Lampwick, contact that app's operator first. ## Who we are NIO Media is the trade name of Niels Oosterhoff, sole proprietorship (eenmanszaak) registered with the Netherlands Chamber of Commerce under number 69855773. | | | |---|---| | Controller | NIO Media | | Address | Roomweg 68, 7523 BR Enschede, the Netherlands | | Chamber of Commerce (KvK) | 69855773 | | VAT | NL002409406B60 | | Privacy contact | [info@niomedia.nl](mailto:info@niomedia.nl), or the [contact form](/contact/) (topic "Privacy") | ## What we collect, why, and for how long The retention periods below are what the software does today. Where a row says "until deleted", the data goes when you delete your account, or the team it belongs to, whichever comes first. Database backups keep it for about 30 more days (see [Backups](#backups)). ### Your account | Data | Purpose | Legal basis | Kept | |---|---|---|---| | Email address, password (stored only as an Argon2 hash), account creation date | Create and run your account, log you in | Contract (art. 6(1)(b)) | Until deleted | | Two-factor secret and recovery codes | Optional second login factor | Contract (art. 6(1)(b)) | Until you turn 2FA off or delete your account | | Team name, team membership and role (owner or member) | Share projects with your team | Contract (art. 6(1)(b)) | Until deleted | | GitHub or Google account id, verified email address and GitHub login name | Only if you choose "Continue with GitHub" or "Continue with Google" | Contract (art. 6(1)(b)) | Until you disconnect the provider or delete your account | | Accounts that never confirm their email address | Signup | Contract (art. 6(1)(b)) | Deleted automatically after 7 days | | API tokens (stored only as a hash), connected apps (OAuth grants for MCP clients) | API and MCP access you set up | Contract (art. 6(1)(b)) | Until you revoke them, they expire, or you delete your account. OAuth access tokens last 1 hour, refresh tokens 60 days | When you log in with GitHub we ask for the `user:email` scope only; with Google, `openid email`. We don't receive your password at those providers. GitHub and Google process your login as independent controllers under their own privacy policies. **What you must provide.** An email address and a password (or a GitHub or Google login) are needed to create an account; billing details are needed for a paid plan. Without them we can't provide the service. Everything else is optional. **No automated decisions.** We make no decisions about you based solely on automated processing that have legal or similarly significant effects (art. 22 GDPR). Plan limits are applied by the software, but they follow from the plan you chose. ### If you are invited to a team When a team owner invites you, we store your email address, the team, the role and who invited you, so we can send you the invitation and let you join. The owner gave us your address; the legal basis is the team's legitimate interest in adding colleagues (art. 6(1)(f)). A pending invitation is kept until it is accepted or revoked, or the team is deleted. The link in the email works for 7 days. ### Sessions and security logs | Data | Purpose | Legal basis | Kept | |---|---|---|---| | Session cookie (a random id; we store only its hash) | Keep you logged in | Contract (art. 6(1)(b)) | 30 days, or until you log out | | Audit log: who did what (logins, 2FA changes, token changes, team and project changes), with the IP address and the channel (web, API, MCP, app) | Security, and to let team owners see who changed what | Legitimate interest in securing accounts and teams (art. 6(1)(f)) | 365 days | | IP address in rate limiting and account lockout counters | Stop password guessing and abuse | Legitimate interest (art. 6(1)(f)) | In memory only, minutes to hours; never written to the database | | Mail throttle: a hash of the email address, the kind of mail and the time | Limit how many mails one address gets | Legitimate interest (art. 6(1)(f)) | 2 days | | Web server access logs: IP address, time, URL and user agent of every request, including ingest and status pages | Operate and secure the service | Legitimate interest (art. 6(1)(f)) | Up to 14 days | | Application logs, which can contain email addresses (for example of failed mails or account actions) | Find and fix errors | Legitimate interest (art. 6(1)(f)) | Up to 14 days | Team owners can see the IP addresses in their team's audit log; members can't. When an account is deleted, its own audit log goes with it, but team audit entries keep the email address and IP address of whoever made a change, so the team's history stays readable for the rest of the 365 days. ### How you use Lampwick | Data | Purpose | Legal basis | Kept | |---|---|---|---| | How you use the app: the pages (address without the query string, ids replaced) and API or MCP operations you call, with status, duration and user agent; errors in the server and in the dashboard in your browser. Linked to your account's id, not your email address. | Find errors and slow pages, see which features are used and how accounts use them over time (funnels, retention), to improve the service. | Legitimate interest in running and improving the service (art. 6(1)(f)) | Requests and errors: as long as our plan keeps records (30 days on Team, 60 on Business). The days you were active: 13 months. Grouped errors (issues): until resolved or deleted. | We measure this with Lampwick itself, on our own servers. It isn't shared with third parties and sets no cookies. The browser script on the dashboard reports errors and console messages only: it sends no page views or custom events. ### Billing | Data | Purpose | Legal basis | Kept | |---|---|---|---| | Plan, subscription status, billing interval, renewal date, Stripe customer and subscription ids, monthly counts of events, browser errors and browser analytics | Run your subscription and enforce plan limits | Contract (art. 6(1)(b)) | Until the team is deleted; monthly usage counts 13 months | | Company name, billing email, billing address, VAT id, payment method | Payment and invoices | Contract and legal obligation (art. 6(1)(b) and (c)) | You enter them in Lampwick or Stripe; we pass them to Stripe, which stores them. We don't keep a copy | | Invoices | Tax and bookkeeping | Legal obligation (art. 6(1)(c)) | 7 years (art. 52 of the Dutch General Tax Act, AWR), by us and by Stripe, also after you delete your account | Payments go through Stripe. Lampwick never sees your full card details: the payment form on the **Billing** page is Stripe's own (Stripe.js). Stripe acts as an independent controller for payment processing and fraud prevention ([Stripe privacy policy](https://stripe.com/privacy)). ### Emails and notifications | Data | Purpose | Legal basis | Kept | |---|---|---|---| | Your email address and the content of service mails: verification, password reset, invitations, alerts, usage and retention notices | Run the service you signed up for | Contract (art. 6(1)(b)) | Alert mails leave our queue within a day; alerts themselves are kept 30 days | | iOS app: device name, platform, Apple push token | Send push notifications for your projects | Contract (art. 6(1)(b)) | Until you log out in the app, remove the device, or delete your account | We send no newsletters or marketing mail. You can turn alert mails off on **Account & security**. ### Support If you email us, we use your message and email address to answer you, based on our legitimate interest in helping customers (art. 6(1)(f)), or on the contract when it is about your account. We keep support mail for 2 years after the last message, except where it is part of our tax records, which we keep for 7 years. | Data | Purpose | Legal basis | Kept | |---|---|---|---| | Contact form: your name, email address, company (optional), topic and message | Answering you | Legitimate interest in answering questions (art. 6(1)(f)), or steps you ask for before a contract (art. 6(1)(b)) | Like support mail: 2 years after the last message. Lampwick stores nothing: the form sends your message to our inbox by email | | Contact form: your IP address | Limit how many messages one network can send | Legitimate interest (art. 6(1)(f)) | In memory only, 1 hour; never written to the database | The [contact form](/contact/) sends you no email; we answer from our inbox. Our logs record only the topic and length of a message, never its text or your address. ### Public status pages Status pages set no cookies and run no scripts. When you view one, your IP address is used in memory to limit requests (300 per 5 minutes) and appears in the web server access logs above. We use it for nothing else. ### The website The marketing website (lampwick.dev, the docs and these pages) sets no cookies, uses no analytics or tracking, and loads nothing from third parties. The theme toggle doesn't store anything. ## Cookies | Cookie | Set by | Purpose | Lifetime | |---|---|---|---| | `__Host-lampwick_session` | The app, after you log in | Keeps you logged in | 30 days (10 minutes during the 2FA step) | | `__Host-lampwick_oauth` | The app, during "Continue with GitHub/Google" | Protects the login flow (state, PKCE, nonce) | 10 minutes | | `lw_hostchecks_notice` | The app, when you dismiss a notice | Remembers that you closed it | 1 year | | `__stripe_mid`, `__stripe_sid` | Stripe.js, only on a team's **Billing** page, for owners | Fraud prevention for payments; Stripe.js also contacts `m.stripe.network` | 1 year and 30 minutes | The Lampwick cookies are strictly necessary or functional for a service you asked for, so they need no consent under the Dutch Telecommunications Act (art. 11.7a Tw). Public status pages and the website set no cookies. ## Who receives your data We use these service providers. Those marked "processor" process data only on our behalf under a data processing agreement. | Provider | Role | What they get | Location | |---|---|---|---| | Hetzner Online GmbH | Processor: servers, database, backups | All data in Lampwick | Germany (EU) | | Lettermint | Processor: sending email | Your email address and the mail content, including messages you send with the contact form | EU (the Netherlands) | | Stripe Payments Europe, Limited | Independent controller for payments; processor for parts of billing | Company name, email address, billing address, VAT id, payment details | Ireland, with transfers to the US | | Apple (Apple Push Notification service) | Processor: push notifications to the iOS app | Push token and notification text: for issues the project name, issue number, error class and message; for alerts their title and body, such as monitor names, failure reasons and URL host and path | US | | GitHub, Google | Independent controllers, only if you log in with them | The login itself | US | Our source code is hosted on GitHub, and GitHub Actions builds and deploys it to our server. GitHub doesn't receive account, billing or monitoring data. We don't sell personal data and don't share it with advertisers. We may disclose data when Dutch law or a court order requires it. ## Transfers outside the EU Lampwick's servers and backups are in the EU. Data reaches the US only through Stripe (payments), Apple (push notifications, if you use the iOS app) and GitHub or Google (if you log in with them). For Apple we rely on the EU-US Data Privacy Framework; for the others on the Data Privacy Framework where the recipient is certified, and on the European Commission's Standard Contractual Clauses otherwise. ## Backups The database is backed up every night. Local backups are kept for 7 days and off-site backups (Hetzner Object Storage, a different location in the EU) for 30 days. Data you delete therefore disappears from backups within about 30 days. We restore a backup only to recover from a failure. ## Security We protect your data with: - TLS for all traffic, with HSTS; - passwords hashed with Argon2, and session ids, API tokens and email links stored only as hashes; - 2FA secrets and alert channel webhook URLs encrypted at rest; - rate limits and account lockout on logins; - strict isolation between teams, and a team audit log; - servers with automatic security updates, a firewall and SSH key-only access. The [Data processing agreement](/legal/dpa/#security-measures) lists the measures in more detail. If a breach puts your personal data at risk, we notify the Autoriteit Persoonsgegevens within 72 hours and tell you without undue delay when the risk to you is high (art. 33 and 34 GDPR). ## Your rights You have the right to access, correct and delete your personal data, to restrict processing and to data portability. You can do much of it yourself: - **Access and portability**: most of your data is available through the [API](/docs/api/), in JSON. You can also ask us by email for a full copy. - **Correction**: change your details on **Account & security**, and billing details on the team's **Billing** page. - **Deletion**: delete your account on **Account & security**. Teams where you are the only member are deleted with all their projects and data. If such a team still has a running subscription, cancel it first; you can delete once the paid period has ended. If you are the only owner of a team with other members, transfer ownership first. **Right to object.** Where we process your data based on legitimate interest (security logs, the audit log, invitations, support, usage measurement), you can object at any time on grounds relating to your situation. We then stop, unless we have compelling legitimate grounds that override your interests, or need the data for legal claims. For any request, email [info@niomedia.nl](mailto:info@niomedia.nl). We answer within one month (extendable by two months for complex requests, in which case we tell you). We may ask you to confirm your identity first. You can file a complaint with the Dutch data protection authority, the [Autoriteit Persoonsgegevens](https://autoriteitpersoonsgegevens.nl), or with the authority in the EU country where you live or work. We'd appreciate the chance to help first. ## Changes We update this policy when the service changes. For material changes we email everyone with an account at least 30 days in advance. The effective date at the top of this page shows the current version. --- Source: https://lampwick.dev/legal/terms/ # Terms of service > The agreement for using Lampwick: plans and payment, limits, acceptable use, your data, switching, availability, liability and Dutch law. ## In short This summary is not part of the terms. - Lampwick is for business use only, by people of 18 or older. - The Free plan is free for as long as it exists. Paid plans are billed in advance per month or year through Stripe and renew until you cancel. - Limits are enforced by the software. We never charge for overage: at a limit, data is refused or features pause. - Your monitoring data stays yours. We process it for you under the [Data processing agreement](/legal/dpa/), and you can export it and switch away. - Only monitor what you own or may monitor, and keep sensitive personal data out of what you send. - You need a legal basis for analytics on your users and must tell them, for example in your privacy policy. Keep personal data out of event names, properties and page paths. - There is no SLA. Our liability is capped. Dutch law applies. ## Who we are NIO Media is the trade name of Niels Oosterhoff, sole proprietorship (eenmanszaak) registered with the Netherlands Chamber of Commerce under number 69855773. Lampwick is a service of NIO Media. "We" and "Lampwick" mean NIO Media; "you" means the business or organization that uses Lampwick, and the person who accepts these terms for it. | | | |---|---| | Provider | NIO Media | | Address | Roomweg 68, 7523 BR Enschede, the Netherlands | | Chamber of Commerce (KvK) | 69855773 | | VAT | NL002409406B60 | | Contact | [info@niomedia.nl](mailto:info@niomedia.nl), or the [contact form](/contact/) | By creating an account or using Lampwick you accept these terms and the [Data processing agreement](/legal/dpa/), and confirm you have read the [Privacy policy](/legal/privacy/). If you accept on behalf of an organization, you confirm that you may bind it. ## What Lampwick is Lampwick is a hosted monitoring service at lampwick.dev. It receives errors, logs, requests, jobs, browser errors, custom events and page views from your apps. From these it shows product analytics on your app's signed-in users: active users, retention, funnels, user paths and feature usage. It also checks the uptime, certificates, domains, DNS and links of the URLs you add, watches your scheduled tasks, sends alerts, and publishes status pages you set up. You use it through the web app, the iOS app, the API and the MCP server. The [docs](/docs/) describe the features. ## Who may use it Lampwick is only for use in the course of a trade, business or profession. By using it you confirm you use it for your trade, business or profession. You must be at least 18 years old. If mandatory law nevertheless treats you as a consumer, you keep the rights that law gives you. ## Your account - Keep your password, 2FA codes, API tokens, ingest tokens and browser keys secret, apart from the browser key in your web pages, which is public by design. You are responsible for what happens with them. Tell us at once if you think someone else has access. - Give a working email address: we send alerts, billing notices and changes to these terms there. - An account that never confirms its email address is deleted after 7 days. - Each team has one or more owners. Owners manage billing, members, ingest tokens and the team's data. The team is the customer for billing; its owners act for it. ## Plans and prices Lampwick has a Free plan and the paid plans Pro, Team and Business. Each plan has limits on events per month, the browser budgets, retention, how long your app users' activity is kept, projects, uptime monitors, the shortest check interval and saved funnels; the Free plan also has one member, and Business has no project or funnel limit. Status pages and the site crawl are in the paid plans. The current plans, limits and prices are on our [pricing page](/pricing/) and on the team's **Billing** page. On the effective date of these terms the prices are €29 (Pro), €79 (Team) and €199 (Business) a month, excluding VAT. Billed yearly, a plan costs 10 times the monthly price. VAT is added where it applies. ### The Free plan - Free costs nothing and has no time limit. Its limits are lower than the paid plans (on the effective date: 500,000 events a month, 3 days retention, 1 project, 3 monitors checked at most every 5 minutes, 1 member). - You can have one active Free team. A second team on Free can't add projects or monitors and receives no data until it is upgraded. - We may change or end the Free plan with 30 days' notice to team owners. ### Price changes We may change prices by email to team owners. For monthly plans, a new price applies from the first billing period that starts at least 30 days after the notice. For yearly plans, it applies at renewal, announced at least 30 days before. If you don't agree, you can cancel before then. ## Payment - Owners manage billing on the team's **Billing** page in Lampwick: start a subscription, change the plan or between monthly and yearly, cancel or resume, edit billing details, update the payment method and see invoices. The Stripe customer portal, linked from the same page, is a fallback. Payments are processed by Stripe. - Subscriptions are paid in advance per month or per year and renew automatically until canceled. - **Plan changes** take effect right away, also in the middle of a period. Before you confirm, the Billing page shows what Stripe will charge. Stripe invoices and charges the prorated difference at once. A change to a cheaper plan or interval gives a credit on your Stripe balance, which is deducted from your next invoices; it is not paid out. - A plan change applies once the payment succeeds. If your bank asks for confirmation (3-D Secure), Stripe holds the change for up to 23 hours until the invoice is paid. - A team can change its plan at most 5 times in 24 hours. - If a renewal payment fails, the team keeps its plan for 7 days while Stripe retries. After that the team moves to Free until the payment succeeds. ## Limits and fair use The software enforces plan limits; we don't send surprise invoices. We never charge for overage. What happens at a limit: - **Events**: they count per team per calendar month (UTC). Owners get an email at 80% and at 100%. At 100%, ingest for the team's projects is refused until the 1st of the next month or until you upgrade; data sent in that time is not stored. - **Browser errors** have their own budget: 10% of the plan's events per month, and at most 1/15 of that budget per day. When it runs out, browser errors are refused until the next day or month; other monitoring keeps working. - **Browser analytics** (custom events and page views sent with the browser script) have a budget of their own, apart from the events and the browser errors: 20% of the plan's events per month (on the effective date: 100,000 Free, 1,000,000 Pro, 5,000,000 Team, 20,000,000 Business), and at most 1/15 of that budget per day. When it runs out, browser analytics pause until the next day or month (UTC): what is sent in that time is not stored. Browser errors and all other monitoring keep working. A project's **Settings** page and the team's **Billing** page say so, for the daily and the monthly limit. Owners get an email when the monthly budget runs out, and when the daily limit is reached, at most once a week. Custom events your server or OpenTelemetry sends count toward the plan's events instead. - **Browser keys** also have caps of their own per minute and per day, such as on new event names and page paths, and at most 2,000 new app user ids a day on browser errors and logs. Past that cap, errors and logs are stored without their user. - **Monitors**: after a downgrade, monitors above the new limit are paused, newest first, and resume when there is room again. Monitors with a shorter interval than the plan allows are checked at the plan's shortest interval. - **Status pages and site crawls**: when a team moves to Free, its status pages go offline and site crawls stop. Both come back after an upgrade. - **Projects and members** above a new limit stay, but you can't add more. - Downgrading deletes nothing directly, but older data then follows the lower retention (see [Your data](#your-data)). The service also has technical limits that protect it and other customers, such as request rates, payload sizes and checks per team. The docs describe them. Don't try to get around limits, for example by creating extra Free teams or accounts. ## Cancellation and refunds - You can cancel a subscription at any time on the **Billing** page. It stays active until the end of the paid period, then the team moves to Free. Until then you can resume it. - A team with a running subscription can't be deleted, and neither can an account that is the only member of such a team. Cancel first; you can delete the team or account once the paid period has ended. - Paid periods are not refunded, in whole or in part, except pro-rata refunds of prepaid fees when we end the service, when we terminate without your fault, when you object to a change of these terms or to a new sub-processor, when a paid feature is removed, or when the agreement is transferred and you object. ## Switching and export You can switch to another provider, or to your own systems, at any time. This section applies the switching rules of the EU Data Act (Regulation (EU) 2023/2854). - **Notice.** You can end the agreement with a notice period of at most 2 months, also for a yearly plan, by canceling on the **Billing** page or by email to [info@niomedia.nl](mailto:info@niomedia.nl). For a yearly plan that ends early, the prepaid remainder of the year is not refunded: that is the early termination fee. - **Transition.** On request, we give you a transition period of up to 30 days after the notice period, during which the service keeps working and we help you move your data. - **Exportable data.** You can export, in JSON, through the [API](/docs/api/), and on request from us for anything the API doesn't cover: - account and team details, members, roles and pending invitations; - projects and their settings; - issues, events (requests, exceptions, logs, queries, jobs, commands, scheduled task runs, custom events, page views and other records) and app users, within your plan's retention; - your app users' activity, and the product analytics reports (users, retention, funnels, route usage and the Requests page), the reports also as CSV; - deploys; - uptime monitors, check results, host check results and site crawl results; - scheduled tasks; - alerts, alert channel settings (without their secret URLs) and delivery logs; - status pages and their notices; - the team and account audit logs. - **Excluded data.** Not exportable, because it is internal to how we run the service or would weaken its security: password hashes, 2FA secrets and recovery codes, hashes of sessions, tokens and links, encrypted webhook URLs and signing secrets, rate-limit and usage counters, internal grouping keys and queues, and our software. - **Retrieval and erasure.** After the notice period (and any transition period) you have at least 30 days to retrieve your data. After that we erase it, and backups roll off within about 30 days. - **No switching charges.** We charge nothing for switching or exporting. - **Data location.** Your data is stored on servers of Hetzner Online GmbH in Germany. We take adequate legal, technical and organizational measures against access by, or transfer to, governments outside the EU where that would conflict with EU or Dutch law, and only give access when a valid order under EU or Dutch law requires it. ## Acceptable use You may not use Lampwick to: - check, crawl or send requests to websites, servers or endpoints that you don't own and have no permission to monitor. This applies to uptime monitors, host checks, site crawls and alert channel webhooks; - attack, overload or test the security of any system, including Lampwick, without written permission; - publish illegal, infringing, misleading, hateful or harmful content on status pages, or impersonate others there; - store or send malware, spam or content you have no right to; - send special categories of personal data (health, religion, biometrics and the like, art. 9 GDPR), criminal data, or payment card numbers; - resell or give access to the service to third parties outside your team, other than through a status page; - break the law, including privacy and export law. Lampwick's requests identify themselves with these user agents: `lampwick-uptime/` (uptime checks), `lampwick-crawler/ (+https://lampwick.dev)` (site crawls), `lampwick-webhooks/` (alert channels) and `lampwick/ (domain expiry checks; +https://lampwick.dev)` (domain checks). The crawler follows `robots.txt` for the pages of the site it crawls; links to other sites are checked with a single request each. Complaints about our requests go to [info@niomedia.nl](mailto:info@niomedia.nl); we may pause monitors that cause them. ## Content on status pages and notices Status pages and their notices are public. You are responsible for what you publish there. This section is how we handle illegal content under the EU Digital Services Act (DSA). - **Report illegal content** by email to [info@niomedia.nl](mailto:info@niomedia.nl), or with the [contact form](/contact/) (topic "Abuse report"). Include the URL of the page, why you think the content is illegal, your name and email address, and a statement that you believe in good faith that the report is accurate. We confirm receipt, decide diligently and objectively, and tell you what we decided. - **Statement of reasons.** If we remove content, unpublish a status page or suspend an account because of illegal content or a breach of these terms, we tell the team owners which measure we took, why, on what facts and rules, and how to contest it. - **Contest a decision** by replying to that email. A person reviews it. - **Points of contact.** For authorities and for users, our single point of contact is [info@niomedia.nl](mailto:info@niomedia.nl), in English or Dutch. ## Your data - **It stays yours.** You keep all rights to the data you send to Lampwick. You give us the rights we need to store, process and show it to provide the service, and to compile aggregated, non-personal usage statistics (such as event counts) for billing, limits and capacity planning. Nothing else. We don't use it to train AI models, sell it or use it for advertising. - **You're responsible for what you send.** Make sure you may send it, configure the integrations' redaction (the Laravel package redacts passwords, tokens and similar fields by default), and keep personal data you don't need out of logs and error messages. The [Data processing agreement](/legal/dpa/) applies to the personal data in it. - **Product analytics.** Keep personal data out of event names, event properties and page paths: the app user id is the only identifier they need. Never put special categories of personal data in them, such as health details (`therapy.booked`). - **Your users.** You need a legal basis for the analytics on your users, and you inform them. Lampwick's analytics are designed so they don't need a consent banner: per-user analytics come from your server, and the browser script's events and page views are anonymous counts without cookies. The [DPA](/legal/dpa/#analytics-and-your-users-devices) explains our position. - **Retention.** Monitoring data is deleted automatically after your plan's retention (on the effective date: Free 3 days, Pro 14, Team 30, Business 60). Your app users' activity (the days each user was active) is kept longer: Free 30 days, Pro 90 days, Team and Business 13 months. Issues are kept until 90 days after their last occurrence. After a downgrade or cancellation the previous retention applies for 30 more days, and owners get an email first. Lampwick is not a backup or archive: export what you need to keep (see [Switching and export](#switching-and-export)). - **Deletion.** Deleting a project, team or account deletes its data right away; backups roll off within about 30 days. A team with a running subscription can be deleted once the paid period ends. ## API, MCP and connected apps - You may use the [API](/docs/api/) and the [MCP server](/docs/mcp/) to automate anything you can do in the app, within the rate limits. - A token or connected app (such as an AI assistant connected through OAuth) acts as your account and sees what your account sees. You are responsible for the tools you connect and what they do with the data. Use read-only tokens where you can: data that comes in through ingest can contain text written by others, and an assistant may act on it. - You can revoke tokens and connected apps at any time on **Account & security**. - We keep `/api/v1` backwards compatible where we can, and announce breaking changes to team owners in advance. ## Our intellectual property Lampwick's software, design, brand and docs belong to NIO Media. You get a non-exclusive, non-transferable right to use the service during the agreement, under these terms. The Lampwick integrations (such as the Laravel package and the browser snippet) are published under the MIT license, which applies to them instead. We may use feedback you give us freely. ## Availability and support We work to keep Lampwick available and fast, but we don't guarantee uninterrupted or error-free service, and we offer no service level agreement (SLA). We may carry out maintenance, preferably announced and outside office hours (CET). Monitoring can miss events: checks or alerts can be late or fail, so don't rely on Lampwick as your only safeguard for critical systems. Support is by email at [info@niomedia.nl](mailto:info@niomedia.nl), on working days, without a guaranteed response time. We may change, add or remove features. When we remove a feature that is part of your paid plan, we tell team owners at least 30 days in advance; if this materially reduces the service you pay for, you may cancel and get a pro-rata refund of prepaid fees. ## Suspension and termination We may suspend or limit an account, team, status page or monitor, without prior notice where needed, if you breach these terms (in particular [Acceptable use](#acceptable-use)), if it puts the service or others at risk, or if the law requires it. Where we reasonably can, we warn you first and give you a chance to fix it. We always send the team owners a statement of reasons: what we did, why, and how to contest it. We may terminate the agreement for a serious or repeated breach, or for unpaid invoices after the grace period. We may also stop offering Lampwick altogether with at least 90 days' notice to team owners; you then get a pro-rata refund of prepaid fees and the export rights of [Switching and export](#switching-and-export). ## Liability 1. **Cap.** Our total liability to you and your organization, for all claims together and on any legal ground (contract, tort, the DPA, art. 82(5) GDPR or otherwise), is limited to the fees you paid us for the service in the 12 months before the event that caused the damage, with a minimum of €100. A series of connected events counts as one event. This limit also protects the owner of NIO Media and anyone we engage to provide the service, who may rely on this section. 2. **Excluded damage.** We are not liable for: lost profit or revenue, lost savings, business interruption, damage to reputation or goodwill, fines and penalties (including those imposed on you by supervisory authorities), claims of your customers or end users, costs of downtime of your systems, and damage resulting from data that is lost, corrupted, late or not received (including missed or late checks and alerts, and data refused, or stored without its user, at a plan or technical limit). 3. **Force majeure.** We are not liable for failures caused by force majeure: circumstances outside our reasonable control, including failures of the internet, power or telecommunications networks, of Apple, Stripe, GitHub or Google, of the systems you monitor, attacks (such as DDoS) we could not reasonably prevent, and government measures. Such failures are not attributable to us (art. 6:75 BW). We are also not liable for damage caused by your own data, settings, or the tools and channels you connect. We are responsible for our sub-processors as for ourselves, within the limits of this section. 4. **Intent and recklessness.** These limits and exclusions don't apply to damage caused by intent or deliberate recklessness of us or our management, or to death or personal injury. 5. **Deadline.** You must report a claim to us in writing within 12 months after you discovered, or reasonably could have discovered, the damage, and start proceedings within 12 months after that report; otherwise the claim lapses. 6. **Your indemnity.** You indemnify us against claims of third parties (including your end users, the owners of systems you monitor, and supervisory authorities) to the extent they result from your breach of these terms or the DPA, or from data you sent us in breach of the law. We tell you promptly about such a claim and let you take part in the defense. ## Changes to these terms We may change these terms. We email team owners at least 30 days before a material change takes effect, and the effective date at the top of this page shows the current version. If you don't agree, you can cancel before the change takes effect and get a pro-rata refund of prepaid fees for the remaining period. Continued use after that date means you accept the new terms. Changes required by law, or that only benefit you, may take effect sooner. ## Governing law and disputes Dutch law applies to these terms and to the agreement, unless mandatory law gives you more. The United Nations Convention on Contracts for the International Sale of Goods (CISG) doesn't apply. Disputes go exclusively to the Rechtbank Overijssel (district court of Overijssel), the Netherlands; cases within the jurisdiction of the kantonrechter go to the kantonrechter of that court. ## Other provisions - If a provision of these terms is invalid, the rest still applies, and the invalid part is replaced by a valid one that comes as close as possible. - You agree in advance that we may transfer this agreement and the DPA (contractsoverneming, art. 6:159 BW) to a legal entity that continues the Lampwick business, including a private limited company (B.V.) owned by NIO Media's owner, or a buyer of Lampwick. We tell you at least 30 days in advance; if you don't agree, you may cancel and get a pro-rata refund of prepaid fees. - These terms and the [Data processing agreement](/legal/dpa/) are the whole agreement. - We expressly reject the applicability of any general terms or purchase conditions of yours, also if you refer to them in an order or another document. - The English text is the binding version. Questions about these terms: [info@niomedia.nl](mailto:info@niomedia.nl). --- Source: https://lampwick.dev/pricing/ # Pricing: four plans per team, from Free to €199 a month > Free, Pro at €29, Team at €79 and Business at €199 a month per team, excluding VAT. Status pages and the site crawl start at Pro. ## Compare plans Per team, excluding VAT. | | Free | Pro | Team | Business | |---|---|---|---|---| | **Price** | | | | | | Per month | €0 | €29 | €79 | €199 | | Per year (2 months free) | €0 | €290 | €790 | €1,990 | | **Monitoring** | | | | | | Events per month | 500k | 5M | 25M | 100M | | Browser errors per month, on top | 50k | 500k | 2.5M | 10M | | History | 3 days | 14 days | 30 days | 60 days | | Issues, requests, jobs, queries, logs, deploys | Yes | Yes | Yes | Yes | | **Uptime** | | | | | | Uptime monitors | 3 | 20 | 100 | 300 | | Shortest check interval | 5 min | 1 min | 30 s | 30 s | | SSL, domain and DNS checks | Yes | Yes | Yes | Yes | | Status pages | No | Yes | Yes | Yes | | Site crawl: broken links, mixed content | No | Yes | Yes | Yes | | **Team** | | | | | | Projects | 1 | 5 | 25 | Unlimited | | Seats | 1 | Unlimited | Unlimited | Unlimited | | Alerts: push, email, Slack, Discord, Teams, webhooks | Yes | Yes | Yes | Yes | | API and MCP server | Yes | Yes | Yes | Yes | | **Product analytics** | | | | | | User activity kept | 30 days | 90 days | 13 months | 13 months | | Page views and custom events per month | 100k | 1M | 5M | 20M | ## Questions ### What counts as an event? Every record Lampwick stores: a request, and each query, outgoing HTTP call, cache event, mail, job, log line and exception in it. Uptime, SSL, domain and DNS checks don't count. ### What happens at the limit? Owners get an email at 80% and at 100% of the month's events. At 100%, ingest for the team's projects is refused until the 1st of the next month or until you upgrade; data sent in that time is not stored. We never charge for overage. ### Do browser errors use my events? No. Browser errors have their own budget on top, 10% of the plan's events, so a noisy page never stops your server monitoring. Page views and custom events from the browser script have a budget of their own too. ### Can I change plans later? Yes, on the team's **Billing** page, at any time. A change takes effect right away: Stripe charges the prorated difference, and a cheaper plan gives a credit on your next invoices. After a downgrade, monitors above the new limit are paused, not deleted, and the previous history stays for 30 more days before the lower one applies. ### My agent set it up. Which plan is that? An agent starts on a trial workspace (72 hours) with its own small limits. Once you claim it, it's a Free team. If you already have one, you can add the projects to it instead; a second Free team receives no data until one is upgraded. See [Set up with your agent](/docs/agents/). ### Is a seat a user of my app? No. Seats are the people in your Lampwick team. Every plan counts events, not the users of your app. ### How do I pay? Through Stripe, monthly or yearly, in euros; yearly billing gets 2 months free. The details are in the [terms](/legal/terms/#payment). --- Source: https://lampwick.dev/product/ # Application monitoring from inside your app and from outside > Exceptions, requests, jobs and logs from inside your app; uptime, SSL, domain and link checks from a URL. With an API and MCP server for your coding agent. Lampwick is application monitoring for web apps. Issues, requests, jobs and logs need your app connected with [OpenTelemetry](/integrations/opentelemetry/), the [Laravel package](/integrations/laravel/) or [HTTP ingest](/integrations/http-ingest/). Uptime, SSL, domain and link checks need only a URL. You can [see the app with example data](/demo/overview.html). ## Issues. Exceptions grouped by class, file and line {#issues} A thousand identical exceptions are one issue with a count. - **Stack trace and context.** Each issue has the stack trace with source code, the users it hit, and the request or job it happened in, with its queries and logs. - **Reopened when it comes back.** A resolved issue reopens when the exception returns, and the team gets a push. - **Deploys.** Every deploy is a marker on the charts and lists the issues first seen after it. Docs: [issues](/docs/issues/), [browser errors](/docs/browser/), [deploys](/docs/deploys/). ## Requests. Status codes, p95 and failures per route {#requests} The overview lists the slowest routes, so you see which one to fix first. - **Timeline per request.** Each request opens a timeline of what it did: its queries, outgoing calls, logs and exceptions. - **Queries.** Database queries grouped, with average and p95 duration, next to outgoing HTTP, cache, mail and notifications. ## Jobs and scheduled tasks. Failed attempts and missed runs {#jobs} Lampwick records every job attempt and every run of a scheduled task. - **Jobs.** Each job class with its processed, released and failed counts and p95 duration. A failed attempt shows the exception that stopped it. - **Scheduled tasks.** Lampwick learns each task's cron expression and timezone from its runs, and alerts when a task misses a run, starts failing or runs too long. There are no heartbeat URLs to create. Schedule health needs the Laravel package today. Docs: [Schedule health](/docs/cron-health/). ## Logs. Every line with the request or job that wrote it {#logs} Filter by level and search the text. - **In the timeline.** A request or job shows its log lines between its queries and exceptions. - **No alerts on log lines.** Exceptions become issues, and issues alert. Log lines don't. ## Uptime, SSL certificate and domain checks. From a URL {#uptime} Paste a URL; your app needs no code. It works for sites you didn't build too. - **Uptime.** A request every 30 seconds to 1 hour, depending on the plan, from one location. After 2 failed checks in a row the team gets a down alert with the reason, such as `HTTP 503`. - **SSL certificate.** Read every 6 to 12 hours. Alerts 14, 7, 3 and 1 days before it expires, and at once when browsers would reject it. - **Domain and DNS.** The expiry date is looked up daily through RDAP, with alerts 30, 14, 7 and 1 days before. NS and MX records are checked hourly. Docs: [uptime](/docs/uptime/), [SSL, domain and DNS](/docs/host-checks/). ## Broken links and mixed content. A daily crawl of up to 250 pages {#broken-links} Lampwick follows the links of your site once a day and checks every one, external links too. From Pro. The crawl reads HTML and doesn't run JavaScript. - **Broken links.** 5xx, 404, other 4xx and connection errors, grouped, with the pages each link is on. - **Mixed content.** `http://` images, scripts, stylesheets and iframes on `https` pages. - **One alert per crawl, for new findings.** Such as "12 broken links (2 new) on 183 pages". Links you know about can be ignored. Docs: [site crawl](/docs/crawler/). ## Status pages. Live status, daily uptime and incident notices {#status-pages} A public page at `app.lampwick.dev/status/your-name` with the monitors you pick. From Pro, up to 10 pages per team. No custom domain. - **Overall status.** Worked out from the monitors and your notices, such as **All systems operational** or **Partial outage**. - **Daily uptime.** A bar per day for each monitor, up to 90 days, under a public name you choose. - **Incident and maintenance notices.** A team owner posts them by hand. Closed notices stay visible for 14 days. Docs: [status pages](/docs/status-pages/). ## Alerts. By push, email, Slack, Discord, Microsoft Teams or webhook {#alerts} Every member of a project's team gets alerts by push in the iOS app and by email. There is nothing to set up. New issues go to push and alert channels, not email. No SMS, phone calls or PagerDuty. - **What alerts.** A monitor down and up again, a certificate or domain about to expire, changed DNS records, new broken links, and scheduled tasks that missed, failed or ran slow. - **Alert channels.** A team's alerts and its new and reopened issues, in Slack, Discord, Microsoft Teams or on your own signed webhook. - **Mute.** Per monitor, task or account. A site that goes up and down sends at most one down alert per 30 minutes. Docs: [alerts](/docs/alerts/). ## MCP server and API. Your coding agent reads issues, requests, logs and monitors {#agents} They are in the [API](/docs/api/) and on an [MCP server](/docs/mcp/) at `/mcp`, read-only unless you choose otherwise. - **Ask from your repository.** "Show me the stack trace of #209 and the request it happened in. Propose a fix." The agent proposes a change and you review it. - **Set up by your agent.** A coding agent can [create a trial workspace](/docs/agents/) without an account and connect your app. You claim it with one link. ## Product analytics. Also included, from the same requests {#analytics} When your requests carry the signed-in user, Lampwick counts active users, retention and funnels from them, and shows how many users an issue hit. There is no tracking script to add. Docs: [users](/docs/users/), [funnels](/docs/funnels/). ## Connect. With OpenTelemetry, the Laravel package or an HTTP POST {#connect} OpenTelemetry for Node.js, Next.js and Python, a package for Laravel, or a POST from anything else: [all integrations](/integrations/). Plans and limits: [pricing](/pricing/). --- Source: https://lampwick.dev/resources/ # Docs, guides, error explanations and comparisons > Docs, guides, common production errors explained, comparisons with other tools and use cases. Every page also comes as Markdown for agents. - [Docs](/docs/) (22 pages): Let your coding agent set up Lampwick and claim the workspace, or sign up yourself and connect your app with Laravel, OpenTelemetry or HTTP ingest. - [Guides](/guides/) (8 pages): How to find out when a Laravel job fails, a scheduled task doesn't run or your app goes down, and how to debug production errors with Claude Code. - [Errors](/errors/) (7 pages): The Laravel and PHP errors developers hit most, each with its real causes, how to tell which one you have, the fix in code and how to catch it. - [Comparisons](/compare/) (9 pages): Lampwick side by side with Nightwatch, Sentry, Flare and Oh Dear, plus lists of Laravel monitoring tools and alternatives, checked on vendor pages. - [Use cases](/for/) (5 pages): For apps built with Lovable, Replit or v0, and agencies running client sites: paste a URL for uptime, SSL, domain and DNS checks, then connect your agent. ## For agents Every page on this site has a Markdown version: add `.md` to its path, or use "View as Markdown" under the title. [llms.txt](/llms.txt) lists them all, and [llms-full.txt](/llms-full.txt) has every page in one file. To set up Lampwick from a coding agent, start with [Set up with your agent](/docs/agents/).