# Snags documentation > Full documentation for Snags (https://snags.com). Generated from the same source as https://snags.com/docs. --- Source: https://snags.com/docs/get-started # Install Snags > Add the extension to Chrome, start an audit on a website, leave your first comment and share the link. ## Add the extension Snags is a Chrome extension. Add it from the Chrome Web Store, then select the Snags icon in the toolbar. Its interface opens in Chrome's side panel. You don't need an account to start commenting. You sign in with your email address when you publish an audit. The people you send an audit to need nothing at all: they open the link in any browser, with no extension and no account. ## Start an audit Open the website you want to review and choose **Start audit** in the side panel. The first time, Chrome asks whether Snags may access websites. Chrome only lets an extension take a screenshot of a tab with that access, and screenshots are the core of a comment. Snags still reads nothing from a website until you start an audit on it, and while an audit runs it records only on that site's pages. If you decline, Snags still works on a tab after you select the Snags icon or press **Alt+Shift+C**, and keeps working on that tab as you move between pages of the same site. When neither is available, Snags draws the screenshot from the page itself, which misses iframes, canvases and images from other sites. ## Leave a comment 1. Choose **Comment**. 2. Click the element that's wrong. Snags outlines it as you point. 3. Say what's wrong and save. That's the whole job. Snags attaches the screenshot, a replay of the moments before, the steps you took, and the console errors and failed requests behind it. See [What gets captured](/docs/what-gets-captured) for everything that comes with a comment. You can also leave a note about the page as a whole rather than one element. Move between pages of the site as you go: one audit can cover the whole site, up to 50 comments of 5,000 characters each. ## Finish and share Choose **Finish and share** when you're done. Snags lists your comments so you can review them before anything is published. Publishing asks you to sign in. Snags emails you a sign-in link, and there's no password. Your screenshots, replays and errors then upload, and you get one link, like `app.snags.com/a/…`, to send to whoever needs to act on it. When you publish you can also: - give the site's repository on GitHub, GitLab or Bitbucket, so each comment's component links to its file (see [Source context](/docs/source-attribution)) - set a password for the link, on Pro and Team - choose a workspace under **Share to**, on Team (see [Workspaces and roles](/docs/teams)) On the free plan you can share 5 audits, and each link lasts 7 days. Without an account, **Download as file** saves the whole audit as one HTML file and uploads nothing. ## What to do next - Read [Share an audit](/docs/sharing) for everything the people you send it to can do. - Connect [Linear, GitHub or Jira](/docs/integrations) to turn comments into issues. - Hand the link to a [coding agent over MCP](/docs/mcp) and let it work through the comments. --- Source: https://snags.com/docs/what-gets-captured # What gets captured > What a comment carries, how masking works, the replay, the automatic checks and how long it's all kept. ## While an audit runs Snags records nothing until you start an audit on a website, and then only on that website's pages. While an audit runs, Snags keeps a short record of the page in your browser's memory: - what changed on the page, for the replay, covering the last 30 to 60 seconds - the steps you took: pages opened, clicks, which fields you typed into, the Enter, Escape and Tab keys, and scrolling - console errors and warnings, uncaught errors, and network requests that failed None of this leaves your browser until you save a comment, and the heavy parts don't leave it until you publish. ## What a comment contains You write what's wrong. The comment then carries: - the page's address and title, with the query string and fragment removed and anything in the path that looks like a secret replaced - the element you clicked: its selector, where it sat on the screen, the start of its visible text and, when the page exposes it, the component that rendered it (see [Source context](/docs/source-attribution)) - a screenshot cropped to the element with some room around it and the element outlined, plus the whole viewport as a **Full view** - a replay of the moments before the comment - the steps you took before it, as a list of plain actions - console errors, uncaught errors and failed requests from about 30 seconds before it - the page's state: browser zoom, scroll position, light or dark colour scheme, reduced motion and connection type - the site's release, commit and environment, when the page announces them Once per audit, Snags also records the browser it ran in: the browser and its version, the operating system, the device type, the screen size and pixel ratio, the language and time zone. It records nothing that identifies a person: no IP address, user-agent string, fonts, other extensions or hardware details. ### Failed requests For a request that failed, Snags keeps the method, address, status and timing, and its request and response headers and bodies, so the comment shows the actual error. Headers that carry credentials, such as `Authorization`, `Cookie` and anything named like a token, session or API key, are replaced with `[redacted]`. Bodies are swept for secrets and cut short. Form uploads and binary bodies are never kept. ## Masking Typed values are never captured. Screenshots and replays hide what's typed into inputs, text areas, dropdowns and editable regions, and your steps say which field you typed into, never what. Console messages, errors, request details and the page text in replays are swept for secrets before they leave the browser: passwords, tokens, API keys, JWTs, card numbers and email addresses are replaced. ### Hide more of a page Snags respects the privacy markers a site may already use: - **Masked:** text inside `data-snags-mask`, `data-rebase-mask`, `data-private`, `.rr-mask`, `data-sentry-mask` and `.fs-mask` becomes asterisks in replays and is covered in screenshots. - **Blocked:** elements marked `data-snags-block`, `data-rebase-block`, `.rr-block`, `data-sentry-block`, `.fs-exclude` and `.lr-hide` become empty boxes. Snags' own toolbar and outlines are always left out of screenshots and replays. :::warning Test before you rely on it Masking can't recognise every kind of private content. Visible page text, console messages and error bodies can still contain personal data. On pages with real people's data, mark the sensitive regions and check a replay before you share it. See the [compliance guide](/docs/compliance-guide). ::: ## Replay The replay is a recording of what changed on the page, not a video. When you save a comment, Snags freezes the recording before it hides its own toolbar for the screenshot, so the replay ends on the moment you were looking at. If you moved between pages of the site, the earlier pages are joined in front, up to 90 seconds back. A replay is at most 4 MB, compressed. People with the link press **Replay** to watch it. Exported files can't play replays, so they animate your steps over the screenshot instead. ## Automatic checks After the first comment on each page, Snags runs three checks on that page and keeps what it finds with the audit: - **Accessibility:** an automated scan of the page for issues such as missing labels and low contrast. - **Layout shift:** how much the page moved while it loaded. - **Broken links:** up to 40 links to other pages of the same site, checked for errors. The findings appear on the audit page, in its Markdown export and to coding agents. ## Where it's kept The text of each comment syncs to Snags as you write it, as a draft. Screenshots, replays and errors stay in your browser (in IndexedDB) until you publish, and then upload with the audit. Drafts that are never published are deleted after 7 days. Free links last 7 days. Pro and Team links last until they're deleted. See [Billing and plans](/docs/billing) for storage limits, and the [privacy policy](https://app.snags.com/privacy) for the details. --- Source: https://snags.com/docs/source-attribution # Source context > How Snags finds the component behind an element, reads the site's release and links to the file in your repository. When you comment on an element, Snags tries to tell whoever fixes it where that element lives in the code: the component that rendered it, the file and line, and the release of the site you were looking at. It shows on the audit page and in what your [coding agent](/docs/mcp) reads. Nothing here needs setting up to comment. It fills in when the site gives Snags something to read. ## The component behind an element Frameworks keep track of which component rendered each element, and development builds keep more of it. Snags reads: - **React** development builds, including the file and line of the JSX - **Vue** 2 and 3 - **Svelte** and **Angular** development builds - the `data-*` attributes that build plugins add to elements, such as Sentry's component annotation, LocatorJS and react-dev-inspector File paths are trimmed to your project, from `src/`, `app/`, `pages/`, `components/` or `lib/` onwards. Production builds usually strip component names. On a live site you'll often get a selector and the element's text rather than a component, unless the site uses an annotation plugin. Audit a staging or preview build when you want the fullest context. ## The site's release Snags reads the site's release and environment from the page itself: - meta tags such as `version`, `release`, `build` and `commit` - common globals, such as Sentry's release, Next.js's build id and app-defined version variables - the hostname, to tell production from staging, preview and local A comment that knows the release turns "when did this break?" into a comparison between two versions. ## Links to your repository When you publish, you can give the site's repository on GitHub, GitLab or Bitbucket. Snags remembers it for that site. Each comment's component then links to its file in the repository. When the page announced its commit, the link opens the file at that commit, so it shows the code you were actually looking at rather than whatever is on the main branch now. ## Treat it as a starting point A component and a file are a lead, not proof of what caused the problem. Shared components, framework code and minified bundles can point at the wrong place, or nowhere at all. --- Source: https://snags.com/docs/sharing # Share an audit > The audit link, passwords and branding, Copy for AI, Markdown and CSV exports, Playwright tests and the offline file. Publishing an audit gives you one link, like `app.snags.com/a/…`. Anyone with it can open the audit in any browser, with no extension and no account. ## What people with the link see Every comment, numbered, with its screenshot, the page it was on and the element it's about. For each comment they can: - open **Full view** to see the whole viewport, and zoom any screenshot - press **Replay** to watch the moments before it - read the steps, the console errors and failed requests, and the browser it happened in - follow a link back to the page, scrolled to the element's text - filter by status, by page, or to comments with errors They can also tick comments off as they go. Ticks are kept in their own browser, for their own checklist. The audit page also lists the [automatic checks](/docs/what-gets-captured#automatic-checks): accessibility issues, layout shift and broken links found on the audited pages. ## Copy for AI **Copy for AI** copies the whole audit as text for an AI assistant: every comment with its page, element, steps and errors. Paste it into any chat. It works on every link, including free ones. To let a coding agent read the audit itself, with the screenshots, and mark comments fixed, connect it [over MCP](/docs/mcp). ## A Playwright test Each comment with recorded steps can be copied as a Playwright test. The test repeats your steps and ends at the element you commented on, for the developer to finish with the assertion that should hold. ## Exports - **Export Markdown** downloads the audit as a Markdown file, ready for a pull request, a wiki or an agent. - **Export CSV** downloads one row per comment, for a spreadsheet. ## Passwords and branding On Pro and Team you can set a password when you publish. People then need it to open the link, and coding agents pass it over MCP. Pro and Team audits also show your name, colour and logo instead of the "Made with Snags" badge that free links carry. ## How long a link lasts Free links last 7 days, then show a page saying the link has expired. Pro and Team links never expire, and upgrading makes every link that's still live permanent. See [Billing and plans](/docs/billing). ## Without an account **Download as file** saves the whole audit as a single HTML file, with every screenshot inside it. Nothing is uploaded, and you can send the file however you like. It doesn't count towards the free plan's 5 shared audits. --- Source: https://snags.com/docs/triage # Status, discussion & fixes > Mark comments open, fixed or won't fix, reply and assign, and check that fixes really landed on the site. An audit is a to-do list for whoever fixes the site. Snags tracks where each comment stands, so nothing slips between the person who reported it and the person who fixed it. ## Status Every comment is **open**, **fixed** or **won't fix**. Anyone signed in on the audit page can change it. Everyone with the link sees the status and who set it, and can filter by it. Status also changes on its own: - when the comment's [Linear, GitHub or Jira issue](/docs/integrations) is closed or reopened - when a [coding agent](/docs/mcp) marks it fixed, with the commit that fixed it A status someone set by hand is never overridden by a tracker. ## Discussion and assignees On Team, the people working on an audit get a discussion under each comment. That's the audit's author and the workspace members who can see its site. - **Reply** to ask or answer a question, and `@mention` someone to bring them in. - **Assign** the comment to the person who should fix it. Replies and assignees are seen only by those people. Others with the link never see them. Snags emails the audit's author, the assignee, earlier repliers and anyone mentioned when someone replies, mentions or assigns them, or changes the comment's status. Every email has a link to stop them. ## Check that it's really fixed A comment marked fixed asks: **Is it fixed on the site?** Open the page, look, and choose: - **Looks fixed** to record that you checked it. - **Still broken** to reopen it, with a reply saying what's still wrong. **Check fixes** in the filter bar walks through every fixed comment nobody has checked yet, one after another. --- Source: https://snags.com/docs/integrations # Trackers & sync > How comments become issues in Linear, GitHub and Jira, what the issue contains, and how status comes back to Snags. Snags turns comments into issues in Linear, GitHub or Jira, and reads their status back. Sending comments to a tracker is part of Pro and Team. ## Connect a tracker Trackers connect on the web, not in the extension: 1. Go to [app.snags.com/integrations](https://app.snags.com/integrations) and sign in with your email. 2. Select **Connect** next to Linear, GitHub or Jira and allow Snags. 3. Choose where new issues go: a Linear team, a GitHub repository or a Jira project. Each connection has one destination. To send issues somewhere else, change the destination and save. Connections are yours. They appear on every audit page you open while you're signed in, including audits other people sent you. ### Shared trackers for a workspace On Team, a workspace can share tracker connections at **app.snags.com/integrations?workspace=…**. Everyone in the workspace except clients can then create issues with them from the workspace's audits. See [Workspaces and roles](/docs/teams). ## Create an issue from a comment On the audit page, choose **Create issue** on a comment and pick the tracker. Anyone who can open the audit, is signed in and is on Pro can do this. Issues always use the connection of the person who creates them, never the audit author's. A shared link can't be used to file issues into someone else's tracker. ### What the issue includes - the comment, as written - the page and the element - the component behind it, when Snags found one - the screenshot - the steps that led up to it - the console errors - a link back to the comment on the audit page Each comment holds one issue. Everyone with the link sees it on the comment, and coding agents see it [over MCP](/docs/mcp). Whoever created the issue, or the audit's author, can unlink it. ## Status sync Every 15 minutes, Snags reads the status of each linked issue from its tracker, using the connection that created it: - **Closed as done** marks the comment fixed. - **Closed as not planned**, cancelled or won't do marks it won't fix. - **Reopened** reopens a comment that the sync had closed. A status someone set by hand on the audit page is never overridden. Changing a comment's status in Snags doesn't change the issue. ## When a tracker stops answering If a tracker's access is removed or expires, its section at [app.snags.com/integrations](https://app.snags.com/integrations) says it isn't answering, with a link to connect it again. --- Source: https://snags.com/docs/github # GitHub > Install the Snags GitHub App on the repositories you pick, and send comments there as issues. ## Connect GitHub 1. Go to [app.snags.com/integrations](https://app.snags.com/integrations) and sign in. 2. Select **Connect** next to GitHub and install the Snags GitHub App on the repositories it should reach. 3. Choose the repository for new issues. Snags uses a GitHub App, so it can only reach the repositories you pick when you install it. Repositories with issues turned off, and archived ones, aren't offered. See [Trackers and sync](/docs/integrations) for what goes into an issue and who can create one. ## Issues From an audit page, **Create issue** on a comment opens an issue in that repository. It carries the comment, the page and element, the component, the screenshot, the steps and the console errors, and links back to the comment. ## Status sync Every 15 minutes Snags reads the issue's state: - Closed as completed: the comment is marked fixed. - Closed as not planned: the comment is marked won't fix. - Reopened: a comment the sync had closed reopens. ## Links to your code Separately from issues, you can give a GitHub repository when you publish an audit. Each comment's component then links to its file, at the commit the site was running. See [Source context](/docs/source-attribution). GitLab and Bitbucket work there too. --- Source: https://snags.com/docs/linear # Linear > Connect Linear, choose a team, and send comments there as issues with the screenshot, steps and errors. ## Connect Linear 1. Go to [app.snags.com/integrations](https://app.snags.com/integrations) and sign in. 2. Select **Connect** next to Linear, sign in to Linear and allow Snags. 3. Choose the team for new issues. If Linear access is removed, the connection says it isn't answering, with a link to connect it again. See [Trackers and sync](/docs/integrations) for who can create issues. ## Issues From an audit page, **Create issue** on a comment creates an issue in that team, with the screenshot, steps and errors filled in, and a link back to the comment in Snags. ## Status sync Every 15 minutes Snags reads the issue's state: - Completed: the comment is marked fixed. - Cancelled: the comment is marked won't fix. - Back to an open state after the sync closed it: the comment reopens. --- Source: https://snags.com/docs/jira # Jira > Connect Jira, choose a project, and send comments there as issues that sync their status back. ## Connect Jira 1. Go to [app.snags.com/integrations](https://app.snags.com/integrations) and sign in. 2. Select **Connect** next to Jira, sign in to Atlassian and allow Snags. 3. Choose the project for new issues. Snags creates a Task, or a Bug where the project has no Task type. If neither exists, it uses the project's first issue type that isn't a subtask. If Jira access is removed or expires, the connection says it isn't answering, with a link to connect it again. See [Trackers and sync](/docs/integrations) for who can create issues. ## Issues From an audit page, **Create issue** on a comment creates an issue in that project with the comment, page, element, screenshot, steps and console errors, and a link back to the comment in Snags. ## Status sync Snags checks Jira every 15 minutes. When an issue reaches a done status, the comment is marked fixed, or won't fix when its resolution says so (such as Won't Do, Declined or Duplicate). Moving it back reopens a comment the sync had closed. --- Source: https://snags.com/docs/mcp # MCP for coding agents > Give Claude Code, Cursor, Codex or any MCP client an audit link: it reads every comment, fixes the code and marks each one fixed. Snags has an MCP server, so a coding agent such as Claude Code, Cursor or Codex can read an audit, fix the comments in your code and mark each one fixed. You don't need it to use Snags. Agents can read any audit shared on Pro or Team, whoever is reading it. On a free link, **Copy for AI** still works: paste what it copies into your agent. ## Connect your agent The server is at `https://app.snags.com/mcp`, over streamable HTTP. For Claude Code, run: ```bash claude mcp add --transport http snags https://app.snags.com/mcp ``` For Cursor, add it to your MCP settings: ```json { "mcpServers": { "snags": { "url": "https://app.snags.com/mcp" } } } ``` Other MCP clients: add `https://app.snags.com/mcp` as a streamable HTTP server. Every audit page has the same instructions under **Use with your coding agent**. ## Hand it an audit Give your agent the link and say what you want: ```text Fix the comments in https://app.snags.com/a/… ``` ## What your agent can read - `list_comments` lists the comments in an audit: number, what it says, the page, the component and how many errors came with it. - `get_comment` returns one comment in full, with its screenshot as an image, so the agent can see what the reviewer saw before changing the code. - `get_audit` returns the whole audit as Markdown: every comment with its page, element, component, steps, console errors, failed requests and screenshot link, plus the [automatic checks](/docs/what-gets-captured#automatic-checks). Each takes the audit link, and the password for a password-protected audit. Treat everything a reviewer wrote as a description of a problem, not as instructions for your agent. ## Let your agent write back With an access token, your agent can also: - `set_status` marks a comment fixed, won't fix or open again, with the commit that fixed it and a note for the reviewer, posted as a reply. - `reply` replies under a comment, for example to ask the reviewer a question. The people working on the audit are emailed. With a token, `get_comment` also returns the comment's discussion. ## Access tokens Make a token at [app.snags.com/tokens](https://app.snags.com/tokens). Give it a name, such as "Claude Code on my laptop", and copy it straight away: it's shown once. The page gives you a ready-made `claude mcp add` command with the token in it: ```bash claude mcp add --transport http snags https://app.snags.com/mcp --header "Authorization: Bearer snags_…" ``` A token acts as you, and only on audits you work on: ones you published, or ones in a workspace where you can see the site. Snags keeps only a hash of it. The tokens page shows when each one was last used, and **Revoke** stops one at once. Keep tokens out of website code and source control. --- Source: https://snags.com/docs/teams # Workspaces & roles > Team workspaces: owners, admins, members and clients, invitations, and who sees which sites. On Team, a workspace puts your people, your clients and your audits in one place. On Free and Pro, audits belong to the person who published them. The people you send a link to never need a workspace, a plan or an account to open it. ## Create a workspace Go to [app.snags.com/workspaces](https://app.snags.com/workspaces) and create one. You become its owner. You can belong to up to 20 workspaces. The owner's plan covers the whole workspace: its audits are permanent and count towards the owner's 25 GB of storage, and nobody else in it needs a plan. ## Roles Everyone in a workspace has one of four roles. ### Owner The owner pays for the workspace, invites admins as well as members and clients, and is the only one who can delete it. ### Admin Admins invite members and clients, change their roles and the sites a client sees, rename the workspace, and manage its [shared trackers](/docs/integrations#shared-trackers-for-a-workspace). They can delete anyone's audit in the workspace. ### Member Members see every audit in the workspace and publish their own into it. They join the discussion on each comment, can be assigned comments, and create issues with the workspace's shared trackers. ### Client Clients see only the sites they were given, and only those sites' audits. They can publish audits of those sites into the workspace, and join the discussion on them. They don't use the workspace's trackers. ## Seats and clients Team has 10 seats for the owner, admins and members. Clients don't take a seat, and they're free and unlimited. ## Invite someone 1. Open the workspace at [app.snags.com/workspaces](https://app.snags.com/workspaces). 2. Enter their email address and choose a role. 3. For a client, choose the sites they'll see. Snags emails them an invitation that lasts 7 days. The link proves their address, so accepting it also signs them in. ## Publish into a workspace When you finish an audit, **Share to** picks the workspace it goes into. Snags remembers the choice for each site. The workspace's audits then appear in the audits list in the extension for everyone who can see them. ## Leave or delete a workspace Anyone but the owner can leave a workspace from its page. The owner can delete it. Each audit then goes back to whoever published it, on their own plan: a free author's audit becomes a free link and expires after 7 days. --- Source: https://snags.com/docs/billing # Billing & plans > Free, Pro and Team, what each includes, the storage and size limits, and what happens when you downgrade. Snags has three plans. Every plan has screenshots, replays and errors on every comment, and nobody you send a link to ever needs a plan or an account. ## Plans | Plan | Monthly | Yearly | Shared audits | Links last | Storage | |---|---|---|---|---|---| | Free | $0 | $0 | 5 in all | 7 days | — | | Pro | $5 | $36 | Unlimited | Until deleted | 5 GB | | Team | $19 | $180 | Unlimited | Until deleted | 25 GB | Prices are in US dollars. ### Free To try it on a site or two. Share up to 5 audits in all, and each link lasts 7 days. Comments, screenshots, replays, errors, **Copy for AI** and Markdown and CSV exports are all included. **Download as file** is unlimited and needs no account. ### Pro For one person, every week. Unlimited links that never expire, password-protected links, your own name, colour and logo, issues in [Linear, GitHub and Jira](/docs/integrations), and [coding agents over MCP](/docs/mcp), reading and writing. ### Team For a studio or team, with its clients. Everything in Pro, plus a [workspace](/docs/teams) with 10 seats for your people and free, unlimited clients, discussion and assignees on every comment, and shared tracker connections. ## Limits | Limit | Free | Pro and Team | |---|---|---| | Comments per audit | 50 | 50 | | Characters per comment | 5,000 | 5,000 | | Size of one audit | 15 MB | 60 MB | | Uploads per day | 300 MB | 3 GB | A screenshot can be up to 1.5 MB, and a replay up to 4 MB. A device can start 30 audits a day. Storage counts everything you've shared, and everything in the workspaces you own. ## Upgrade Upgrade from the extension. Checkout and payment are handled by Stripe, and Snags never sees your card details. Upgrading makes every link that's still live permanent, including free links that were about to expire. To move between Pro and Team, change plan, update your card, get invoices or cancel, use **Manage billing** in the extension. ## Downgrade or cancel When a paid plan ends, your permanent links get a grace period: they keep working for 30 days, then expire like free links. Upgrade again within that time and they stay permanent. ## Workspaces and agents A workspace's audits use its owner's plan. Members and clients don't need one. Coding agents can read any audit shared on Pro or Team, whoever is reading it. On a free link, **Copy for AI** still works. --- Source: https://snags.com/docs/compliance-guide # Privacy & compliance guide > Use Snags responsibly on websites with real people's data: where to audit, what to mask, who sees an audit and how to delete it. A practical guide to using Snags on websites that hold real people's data: what Snags records, the choices that are yours, and the controls Snags gives you. This guide is informational, not legal advice. For what Snags collects and keeps, see the [privacy policy](https://app.snags.com/privacy). ## 1. What Snags records, and whose Snags records only your own browser, and only while you run an audit on a site. It is not a tool that watches a website's visitors: nothing is added to the site, and nobody else's session is ever recorded. What can end up in an audit is what was on your screen: the page, the element you commented on, the console errors and failed requests your browser saw, and a replay of the moments before. If that page showed someone's personal data, such as a customer record in an admin panel, the audit can contain it. ## 2. Who is responsible for what You decide which sites to audit, what to comment on and who to send the link to. If an audit captures other people's personal data, that makes you, or the organisation you audit for, responsible for it under laws such as the GDPR. Snags stores and serves the audit on your behalf, and gives you the controls below to keep personal data out and to remove it. ## 3. Prefer staging and test data Most audits don't need real people's data. Staging, preview and local builds usually hold test data, give Snags fuller [source context](/docs/source-attribution), and keep your compliance surface small. When you must audit a live site, prefer a test account over a real customer's. ## 4. What is always masked - **Typed values are never captured.** Screenshots and replays hide what's typed into inputs, text areas, dropdowns and editable regions. Steps record which field you typed into, never what. - **Secrets are swept** from console messages, errors and request details before they leave your browser: passwords, tokens, API keys, JWTs, card numbers and email addresses are replaced with `[redacted]`. - **Credential headers are redacted:** `Authorization`, `Cookie` and any header named like a token, session or API key. - **Addresses are cleaned:** query strings and fragments are removed, and path segments that look like secrets are replaced. ## 5. Mark sensitive regions Snags respects the privacy markers many sites already use for session-recording tools, so a site that has marked its sensitive regions is covered on day one: - **Mask** (text becomes asterisks, screenshots cover it): `data-snags-mask`, `data-rebase-mask`, `data-private`, `.rr-mask`, `data-sentry-mask`, `.fs-mask` - **Block** (the element becomes an empty box): `data-snags-block`, `data-rebase-block`, `.rr-block`, `data-sentry-block`, `.fs-exclude`, `.lr-hide` If you build the site, add `data-snags-block` to any region that renders personal data you'd never want in an audit. Masking can't recognise every kind of private content. Before sharing an audit of a page with real data, open a comment's screenshot and replay and check them. ## 6. Avoid special-category data Snags isn't meant for special-category data, such as health or biometric data. Don't audit pages that show it, or block those regions first. ## 7. Share with care An audit link opens for anyone who has it. Before you send one: - On Pro and Team, set a **password** on the link. - On Team, publish into a [workspace](/docs/teams), and give clients only the sites they need. - Remember that an issue created in [Linear, GitHub or Jira](/docs/integrations) carries the comment, screenshot, steps and errors into that tracker, where its own permissions and retention apply. - Coding agents read audits [over MCP](/docs/mcp) when given the link. Agents act on the audit's content, so share links with agents as you would with a person. ## 8. Retention and erasure ### How long audits are kept - Free links last 7 days, then their comments, screenshots and replays are deleted. A record that the link existed, its title, is kept for 180 days so the link can say it expired. - Pro and Team links last until they're deleted, or the account or workspace is. - Drafts that are never published are deleted after 7 days. ### Deleting - Delete a comment, or a whole audit, in the extension. A workspace's owner and admins can delete any audit in it. - **Account → Your data** in the extension exports everything you've shared as a file, or deletes your account with its audits, screenshots, replays, replies, tracker connections and access tokens. Links stop working. - Issues already sent to a tracker stay there, so delete those in the tracker too. ## 9. Who else handles it Snags is hosted on Cloudflare, which stores audits. Amazon SES delivers sign-in emails and notifications, and Stripe handles payments. Trackers you connect receive the comments you send them. Snags doesn't sell data, show ads or use what you capture to train AI models. ## 10. Contact Questions about using Snags responsibly, or a request under data protection law? Email [privacy@snags.com](mailto:privacy@snags.com). --- Source: https://snags.com/docs/security # Security & privacy > What Snags can reach in your browser, who can open an audit, sign-in and tokens, and how secrets are kept out. ## What the extension can reach Snags asks for access to websites once, when you start your first audit, because Chrome only lets an extension screenshot a tab with that access. It still reads nothing from a site until you start an audit there, and it adds its toolbar only to that site's pages. To offer to start an audit, the side panel reads the current tab's address and title. Nothing from it is kept. What Snags records during an audit, and what it masks, is in [What gets captured](/docs/what-gets-captured). ## Who can open an audit An audit link opens for anyone who has it, without an account. That's what makes it easy to send, so treat a link like the content in it. - On Pro and Team, a link can require a **password**. Coding agents pass it over MCP. - Free links expire after 7 days. - Replies, assignees and who checked a fix are seen only by the audit's author and the workspace members who can see its site, never by others with the link. - In a [workspace](/docs/teams), clients see only the sites they were given. ## Signing in There are no passwords. You sign in with a link sent to your email address, in the extension and on [app.snags.com](https://app.snags.com) alike. Sign-in pages are protected against automated abuse, and sign-in emails are limited to 5 per address per hour. On the web, a sign-in lasts in an HttpOnly cookie, and requests that change something must come from Snags' own site. Sign-ins end after 90 days without use. ## Trackers and agents - Tracker connections are encrypted on Snags' servers. Linear and Jira connect with OAuth. GitHub connects through a GitHub App, so Snags reaches only the repositories you pick. - An issue always uses the connection of the person who creates it, so a shared link can't be used to file into someone else's tracker. - [Access tokens](/docs/mcp#access-tokens) for coding agents act as you, only on audits you work on. Snags keeps only a hash of each one, and you can revoke them at any time. ## Data handling Secrets are swept out in the browser, before anything uploads: credential headers, tokens, keys, card numbers and email addresses in errors and requests. Uploads are checked against size limits as they arrive. Screenshots, replays and errors stay in your browser until you publish. See the [compliance guide](/docs/compliance-guide) for using Snags on pages with real people's data, and the [privacy policy](https://app.snags.com/privacy) for what's kept and for how long. Report security concerns to [privacy@snags.com](mailto:privacy@snags.com). --- Source: https://snags.com/guides/what-is-a-har-file # What is a HAR file? How to make, read and share one safely > A HAR file logs every request your browser made. How to export one in Chrome, Firefox, Edge or Safari, read it, and strip the secrets before you share it. A HAR file (HTTP Archive) is a JSON file that logs every network request a browser tab made: the URL, the request and response headers, cookies, status codes, timings and usually the response bodies. Browsers export it from their developer tools, and support teams use it to see exactly what failed between your browser and their servers. It's plain text, so any editor will open it. It's also big. A couple of minutes on a modern web app produces hundreds of entries, and most of them are fonts, images and analytics pings that nobody will ever read. ## How to get a HAR file in Chrome, Firefox, Edge and Safari Every browser works the same way. You open the developer tools, switch to the Network tab, reproduce the problem and export. The step people miss is keeping the log across page loads. Without it, the recording resets the moment the page navigates, and the request that broke disappears with it. | Browser | Open the Network tab | Keep the log | Export | | --- | --- | --- | --- | | Chrome | F12, or Cmd+Option+I on a Mac, then Network | Tick "Preserve log" | The download arrow in the Network toolbar | | Edge | Same as Chrome | Tick "Preserve log" | Same as Chrome | | Firefox | F12, or Cmd+Option+E on a Mac | Gear icon, then "Persist Logs" | Right-click any request, "Save All As HAR" | | Safari | Turn on Settings, Advanced, "Show features for web developers", then Develop, Show Web Inspector, Network | "Preserve Log" | The Export button in the Network tab | Open the Network tab before you start, then go through the steps from just before the point where things break. Ten seconds recorded around the failure beats ten minutes of browsing. The developer has to find the needle either way. ## How to open and read a HAR file The easiest way to open a HAR file is to drag it onto the Network tab of Chrome's developer tools. It loads exactly like a live recording, and the file never leaves your machine. If you'd rather use a web page, our [HAR analyzer](/tools/har-checker) lists the failed requests and flags secrets without uploading the file, and Google's [HAR Analyzer](https://toolbox.googleapps.com/apps/har_analyzer/) works too. Reading one is mostly knowing where to look. I start with failures: anything with a 4xx or 5xx status, plus status 0, which means the request never finished (blocked by CORS, an ad blocker or a dropped connection). Then I read the response body of the first failure. Errors cascade, and the first one usually explains the ones after it. After that come timings, because one request that takes 30 seconds explains "the page just hangs" better than any screenshot. Last are redirect chains, which are where login loops live. If you have [jq](https://jqlang.org) installed, two one-liners get you most of the way. This one lists every failed request: ```bash jq -r '.log.entries[] | select(.response.status >= 400 or .response.status == 0) | "\(.response.status) \(.request.method) \(.request.url)"' session.har ``` And this one shows the five slowest: ```bash jq -r '.log.entries | sort_by(-.time) | .[:5][] | "\(.time | round)ms \(.request.url)"' session.har ``` ## Is it safe to share a HAR file? Not as a raw export. A HAR file can contain your cookies, authorization headers and session tokens, and anyone holding a live session token can act as you until it expires. Recent Chrome versions strip cookies and auth headers by default, but tokens in URLs, request bodies and custom headers still get through. Check the file before you send it. This isn't a theoretical risk. Between 28 September and 17 October 2023, an attacker had access to Okta's support case system and the files that 134 customers had uploaded for troubleshooting. [According to Okta](https://sec.okta.com/articles/2023/11/unauthorized-access-oktas-support-case-management-system-root-cause/), some of those were HAR files with session tokens in them, and the attacker used the tokens to hijack sessions at five customers. Those customers had done exactly what support asked them to do. Chrome and other Chromium browsers have exported a sanitized HAR by default since version 130, which drops the Cookie, Set-Cookie and Authorization headers. The full version is still there behind a setting under the developer tools' Network preferences. That closes the most common leak. It doesn't catch an API key in a query string, a token in a custom header like `X-Auth-Token`, or a password inside a form post. Don't assume other browsers sanitize anything. To clean a file yourself, this removes cookies and the usual auth headers from every entry: ```bash jq '.log.entries |= map( .request.cookies = [] | .response.cookies = [] | .request.headers |= map(select(.name | ascii_downcase | IN("cookie", "authorization", "x-api-key", "x-auth-token") | not)) | .response.headers |= map(select(.name | ascii_downcase | IN("set-cookie") | not)) )' session.har > session.clean.har ``` Then search what's left for anything that still looks like a credential: ```bash grep -oiE '(token|bearer|api[_-]?key|session)[^"]{0,40}' session.clean.har | sort -u ``` Google's open source [har-sanitizer](https://github.com/google/har-sanitizer) does the same thing through a web page and also handles query strings and post parameters. If you'd rather not touch a terminal at all, the [HAR analyzer](/tools/har-checker) runs both steps in your browser and hands you a redacted copy. No sanitizer is perfect, and that includes mine. Read what's left before you trust the file. One more habit that costs nothing: once support has the file, sign out of the account you recorded. On most apps that ends the session, and every token in the file goes dead with it. ## For support teams Most people searching for this were asked for a HAR file. If you work in support and you're the one asking, the Okta breach is worth a minute of your team's time. The files that leaked were sitting in a support system, exactly where customers had been told to send them. Three habits cover most of the risk: - Ask for the four facts in the next section first, and only ask for a HAR when they aren't enough. - When you do need one, ask for a redacted copy. Send the customer to a [HAR checker that runs in their browser](/tools/har-checker) and they can make one in a minute. - Treat every HAR like a password. Keep it out of shared tickets and chat, delete it when the case closes, and ask the customer to sign out once you have it. ## What should support ask for instead of a HAR file? Usually just the one request that failed: its method, URL, status, response body and the time it happened, plus the browser console error if there was one. That's almost always enough to find the bug in server logs, and none of it can sign anyone in. | Ask for | Why it helps | | --- | --- | | Method, URL and status of the failing request | Narrows the problem to one endpoint | | Its response body | Usually contains the real error message | | The console error and stack trace | Points at a file and a line | | The time it happened and the browser | Lets you find it in server logs | I think a whole HAR is the wrong default for most support requests. It asks a customer to run developer tools, then hands the support team a file holding that customer's live session, all to get at four facts. Those are the same four that the evidence section of a [good bug report](/guides/bug-report-template) asks for. They're also why a [Snags](/docs/what-gets-captured) comment carries the failed requests from the moments before it, with their headers and bodies, rather than anyone being asked for a HAR. Credential headers are redacted and bodies are swept for tokens before anything leaves the browser, so whoever fixes it gets the failing request and nobody has to export a thing. Whatever tool you use, the principle holds: collect the failure, not the session. --- Source: https://snags.com/guides/open-the-console # How to open the console in Chrome, Edge, Firefox and Safari > The keys that open the console and developer tools in Chrome, Edge, Firefox and Safari on Windows and Mac, plus the setting Safari needs switched on first. To open the console in Chrome, press Ctrl+Shift+J on Windows or Cmd+Option+J on a Mac. The developer tools open with the Console tab already selected. Edge uses the same keys. Firefox uses Ctrl+Shift+K or Cmd+Option+K, and Safari needs its developer features switched on before Option+Cmd+C does anything. ## Console shortcuts for every browser These open the developer tools straight on the console, the panel where the page's errors and warnings show up in red and yellow. | Browser | Windows | Mac | | --- | --- | --- | | Chrome | Ctrl+Shift+J | Cmd+Option+J | | Edge | Ctrl+Shift+J | Cmd+Option+J | | Firefox | Ctrl+Shift+K | Cmd+Option+K | | Safari | Not available on Windows | Turn on "Show features for web developers" first, then Option+Cmd+C | Chrome, Edge and Firefox list these keys in their own developer tools documentation. Apple's documentation only gives Option+Cmd+I, which opens Safari's Web Inspector on its last tab. Option+Cmd+C is the key Safari shows next to Show JavaScript Console in its Develop menu. If it ever does nothing, Option+Cmd+I and a click on the Console tab gets you to the same place. ## What's the Chrome developer tools shortcut? F12 or Ctrl+Shift+I opens Chrome's developer tools on Windows, and Cmd+Option+I does it on a Mac. Either one opens whichever panel you used last. Ctrl+Shift+C, or Cmd+Shift+C on a Mac, opens the tools with the element picker switched on. Edge uses exactly the same keys, because it's built on the same engine. | What it opens | Windows | Mac | | --- | --- | --- | | The last panel you used | F12 or Ctrl+Shift+I | Cmd+Option+I, or Fn+F12 | | The Console panel | Ctrl+Shift+J | Cmd+Option+J | | The Elements panel, with the picker on | Ctrl+Shift+C | Cmd+Shift+C | On most Mac keyboards the top row controls brightness and volume, which is why F12 needs Fn held down. F12 or Ctrl+Shift+I again, or Cmd+Option+I on a Mac, closes them. Firefox mostly matches Chrome, with one trap. Ctrl+Shift+I, Cmd+Option+I and F12 open its developer tools, but Ctrl+Shift+J doesn't open the page's console the way it does in Chrome. In Firefox it opens the Browser Console, a separate window that collects messages from the whole browser and its extensions. If someone sends you a Firefox console screenshot full of warnings about add-ons you've never heard of, that's the window they opened. ## How to open the console in Safari Safari hides its developer tools until you ask for them. Choose Safari, then Settings, open the Advanced tab and tick "Show features for web developers". A Develop menu appears in the menu bar. Choose Develop, then Show JavaScript Console, or press Option+Cmd+C. Older versions of Safari called the setting "Show Develop menu in menu bar". Once the setting is on, right-clicking anything on a page also offers Inspect Element, which opens the Web Inspector on the Elements tab. The Console tab is next to it. Safari on iPhone has no console of its own. Turn on Settings, Apps, Safari, Advanced, Web Inspector on the phone, plug it into a Mac with a cable, and the phone's open pages appear in the Mac's Develop menu. It's clumsy, and it's still the only way to see what a page is doing on an iPhone. ## How to open developer tools without a keyboard shortcut Every browser also has a menu path, which is easier to talk someone through on a call than a three-key shortcut. | Browser | Menu path | | --- | --- | | Chrome | The three-dot menu, then More tools, then Developer tools | | Edge | The three-dot menu, then More tools, then Developer tools | | Firefox | The three-line menu, then More tools, then Web Developer Tools | | Safari | Develop, then Show Web Inspector, once developer features are on | Right-clicking the page and choosing Inspect works in all four too. It opens the Elements panel rather than the console, so tell people to click the Console tab afterwards. ## Finding the error once the console is open The console fills up fast on a busy site, and most of it isn't the problem. Errors are the red lines. In Chrome and Edge, the levels menu above the messages can hide everything but errors, and Firefox has an Errors button in the same spot. Click the small arrow next to an error to expand its [stack trace](/guides/what-is-a-stack-trace), which shows the code that was running when it broke. I'd also tick Preserve log in Chrome or Persist Logs in Firefox before reproducing anything. Without it, the console clears every time the page navigates, and a form that submits and reloads wipes the very error you were trying to catch. The two errors you'll see most often have their own guides: [Cannot read properties of undefined](/guides/cannot-read-properties-of-undefined) and [Failed to fetch](/guides/failed-to-fetch). ## For support teams Send the keys for the customer's own browser and system, not "open the developer tools", which means nothing to most people. Ask for the console with the red errors expanded, and copied as text if they can manage it, because a screenshot usually cuts off the one line that matters. If they're on Firefox and the screenshot is full of extension noise, ask them to try Ctrl+Shift+K instead. --- Source: https://snags.com/guides/what-is-a-source-map # What is a source map? How browsers find them and when to ship them > A source map links minified JavaScript back to your original code. What's inside one, the three ways a browser finds it, and whether to publish maps in production. A source map is a JSON file that links minified production JavaScript back to the original source files. For any line and column in the built bundle, it records which source file, line, column and name it came from. Developer tools and error trackers use it to turn an unreadable [stack trace](/guides/what-is-a-stack-trace) back into your own code. ## What's inside a source map? Here's a three-line function built with esbuild, minified, with source maps turned on. ```js export function formatTotal(order) { return order.totals.grand.toFixed(2); } ``` The build writes two files. The bundle has the code squeezed onto one line, with a comment at the end: ```js function o(t){return t.totals.grand.toFixed(2)}export{o as formatTotal}; //# sourceMappingURL=cart.js.map ``` And `cart.js.map` next to it holds this: ```json { "version": 3, "sources": ["../src/cart.js"], "sourcesContent": ["export function formatTotal(order) {\n return order.totals.grand.toFixed(2);\n}\n"], "mappings": "AAAO,SAASA,EAAYC,EAAO,CACjC,OAAOA,EAAM,OAAO,MAAM,QAAQ,CAAC,CACrC", "names": ["formatTotal", "order"] } ``` `sources` lists the original files. `names` holds the identifiers the minifier renamed, which is how `o` becomes `formatTotal` again. `mappings` is the actual map: each comma-separated chunk is a run of Base64 numbers saying "this column in the output came from that file, line, column and name". You'll never read it by hand. `sourcesContent` is the one to notice. It's optional, and it embeds your original source code, comments included, inside the map. Whether you publish that field is most of the decision later on this page. ## How does a browser find the source map? A browser or tool finds a source map in one of three ways: a `sourceMappingURL` comment at the end of the JavaScript file, a `SourceMap` HTTP header on the response that served the file, or a map inlined as a base64 data URL in that same comment. A relative URL resolves against the JavaScript file's own address. | Method | What it looks like | Good for | | --- | --- | --- | | Comment | `//# sourceMappingURL=cart.js.map` on the last line | The default in almost every bundler | | HTTP header | `SourceMap: /assets/cart.js.map` | Adding maps without rebuilding the files | | Inline | `//# sourceMappingURL=data:application/json;base64,eyJ2…` | Local development only | CSS uses the same idea with a `/*# sourceMappingURL=… */` comment. The older `X-SourceMap` header still works in most tools, but `SourceMap` is the current name. Inline maps belong in development. They put the whole map inside the bundle, so every visitor downloads it, and a map is usually bigger than the code it describes. A separate `.map` file costs your visitors nothing, because browsers only fetch it when someone opens the developer tools. ## Should you ship source maps to production? For most web apps, yes. Your JavaScript is already public, because every visitor downloads it, and minification shrinks files rather than protecting anything. A public source map gives away your original names, file layout and comments. It doesn't give away anything a determined person couldn't already work out from the bundle, and it makes every production error readable. That's my view, and there are honest reasons to disagree. If the comments say things you wouldn't put in a public repo, if the client-side code is genuinely what customers pay for, or if file names announce features you haven't launched, publishing `sourcesContent` hands all of that over neatly formatted. Note what isn't on that list: secrets. An API key in a source map was already in the bundle, and the map isn't the problem. | Approach | Who can read your source | Readable traces in developer tools | Works with tools that fetch maps | | --- | --- | --- | --- | | Public maps | Anyone who looks | Yes, for everyone | Yes | | Hidden maps, uploaded to an error tracker | Only that tracker | No | No | | No maps | Nobody | No | No | Hidden maps are the usual middle ground: the bundler writes the `.map` files but leaves out the comment, and you upload the maps to your error tracker and keep them off the public site. That works if one tool is all you need. It also means anything else that reads maps from your site finds nothing, and your own developer tools show minified code in production unless you load a map by hand. There's a half step too. A map without `sourcesContent` still turns `o` back into `formatTotal` at `src/cart.js:2`, but doesn't publish the code itself. You can open the file locally. ## How to turn source maps on Every major build tool has a setting for public maps, and most have a hidden variant. | Tool | Public maps | Hidden maps | | --- | --- | --- | | Vite | `build.sourcemap: true` | `build.sourcemap: 'hidden'` | | webpack | `devtool: 'source-map'` | `devtool: 'hidden-source-map'` | | Rollup | `output.sourcemap: true` | `output.sourcemap: 'hidden'` | | esbuild | `--sourcemap` | `--sourcemap=external` | | Next.js | `productionBrowserSourceMaps: true` | No built-in option | ## How to check your source maps work Open your site, find the main bundle in the Network panel, and check its last line for the comment. Then request the map yourself: ```bash curl -sI https://shop.example.com/assets/index-B7c2x9Qe.js.map ``` You want a `200` and a JSON content type. The usual failures are boring. The deploy step skipped `.map` files, a CDN rule blocked them, or the map on the server came from a different build than the bundle it sits next to, so every line points somewhere slightly wrong. That last one is the nastiest, because nothing errors. Build the bundle and its map in the same step, and deploy them together. ## For support teams If engineering says an error report is "minified and useless", it usually means the source maps aren't reachable from the place that's reading them. That's a build setting, not a missing piece of the report, and it's worth raising once rather than living with. --- Source: https://snags.com/guides/what-is-a-stack-trace # What is a stack trace? How to read one in JavaScript > A stack trace lists the function calls that were running when an error happened. How to read a JavaScript stack trace, and why production ones need a source map. A stack trace is the list of function calls that were in progress when an error happened, newest first. Each line, called a frame, names a function and the file, line and column it had reached. Read it from the top: the first frame is where the error was thrown, and every frame below it called the one above. ## What does a stack trace look like? Here's a small checkout script. `onCheckout` calls `loadCart`, which fetches an order and hands it to `formatTotal`. ```js function formatTotal(order) { return order.totals.grand.toFixed(2); } async function loadCart(id) { const res = await fetch(`/api/orders/${id}`); const order = await res.json(); return formatTotal(order); } async function onCheckout() { await loadCart(42); } ``` The API answered with an order that had no `totals`. This is the stack trace Chrome printed, word for word apart from the address: ```text TypeError: Cannot read properties of undefined (reading 'grand') at formatTotal (https://shop.example.com/assets/cart.js:2:23) at loadCart (https://shop.example.com/assets/cart.js:8:10) at async onCheckout (https://shop.example.com/assets/cart.js:12:3) ``` The first line is the error itself: its type, then the message. Everything after it is the stack. ## How to read a stack trace Start at the top frame. `formatTotal (https://shop.example.com/assets/cart.js:2:23)` says the error was thrown inside `formatTotal`, in `cart.js`, on line 2, at column 23. Columns count characters from 1, and column 23 lands on `grand`, the property that couldn't be read. Then read down. Line 8 of `loadCart` called `formatTotal`, and line 12 of `onCheckout` called `loadCart`. That's the stack: who called whom, at the moment it broke. On a real site the top few frames are often someone else's code, like a framework, a date library or a polyfill. Skip down to the first frame in a file you wrote. That's usually the line to open first. Usually, not always. The top frame is where the code broke, which isn't always where the mistake is. `formatTotal` did nothing wrong here. It was handed an order without totals, and the real question is why the API sent one, which no frame in this trace can answer. I'd read every stack trace as a map of where to start looking, never as the answer. ## What are async frames in a stack trace? An async frame is a line marked `async` that shows an `await` the error passed through on its way up. Chrome and Node print `at async onCheckout`, Firefox prefixes the function with `async*`, and Safari writes `async onCheckout`. Without them, the stack would stop at the first `await`. They only follow `await`. A callback from `setTimeout`, an event listener or a `.then()` chain starts with a fresh stack, so the trace stops where that callback began, even though something else scheduled it. Chrome's console can show those scheduled-from links when you expand an error there, but the `error.stack` text your error logging receives doesn't have them. Chrome and Node also keep only 10 frames by default. Deep framework call chains can lose the bottom of the stack, including the code that started it. `Error.stackTraceLimit = 50` raises the limit for errors created after you set it, which is handy while debugging locally. ## How stack traces differ between browsers The same error from the same code reads differently in each engine. These are the top lines Firefox and Safari printed for the example above. | Browser | Error message | Top frame | | --- | --- | --- | | Chrome and Edge | `Cannot read properties of undefined (reading 'grand')` | `at formatTotal (…/cart.js:2:23)` | | Firefox | `can't access property "grand", order.totals is undefined` | `formatTotal@…/cart.js:2:3` | | Safari | `undefined is not an object (evaluating 'order.totals.grand')` | `formatTotal@…/cart.js:2:22` | The columns disagree because each engine points at a slightly different spot on the same line. Firefox and Safari write `function@file:line:column` with no "at" and no brackets. Notice that their messages name the whole expression that was undefined, which Chrome's doesn't. ## Why production stack traces are unreadable Production JavaScript is minified. The build squashes every file onto a few long lines and renames functions to single letters, so the same error from a minified build of the example looks like this: ```text TypeError: Cannot read properties of undefined (reading 'grand') at n (https://shop.example.com/assets/index-B7c2x9Qe.js:1:37) at a (https://shop.example.com/assets/index-B7c2x9Qe.js:1:133) at async r (https://shop.example.com/assets/index-B7c2x9Qe.js:1:157) ``` Function `a` at line 1, column 133 tells you nothing, and in a real bundle that line is hundreds of kilobytes long. A [source map](/guides/what-is-a-source-map) is the file that translates it back to `formatTotal` in `cart.js` on line 2. Browser developer tools apply it automatically when the map is published, but the `error.stack` text sent to an error tracker stays minified, so the tool has to apply the map itself. That's why a minified trace in a bug report is worth pairing with the element and component it happened on: Snags attaches both to a comment, with the site's release, so you know which build and which map to open ([how source context works](/docs/source-attribution)). ## For support teams When a customer or teammate sends an error, ask for the whole stack trace as text, not a screenshot of the first line. The message says what broke. The frames say where, and engineering needs both. In the console, click the arrow next to the red error to expand it, then select and copy everything down to the last frame. --- Source: https://snags.com/guides/cannot-read-properties-of-undefined # Cannot read properties of undefined: causes and fixes > What "Cannot read properties of undefined" means, the four things that cause it in production, and how to tell which one from a bug report before you fix it. "Cannot read properties of undefined" is the TypeError Chrome, Edge and Node.js throw when code reads a property from a value that is undefined. In `order.totals.grand`, it means `order.totals` was undefined, and the name in brackets is the property it couldn't read. The fix starts with finding out why that value was missing. ## What the error means Here's the full message from Chrome: ```text TypeError: Cannot read properties of undefined (reading 'grand') ``` The name in brackets is the property the code tried to read, not the thing that was missing. People search their code for `grand` and stare at it. The problem is whatever sits to its left. In `order.totals.grand`, that's `order.totals`. Older versions of Chrome and Node worded it as `Cannot read property 'grand' of undefined`, and it means the same thing. Firefox and Safari report the same mistake differently, and honestly more helpfully, because they name the expression that was undefined: | Browser | The message for `order.totals.grand` | | --- | --- | | Chrome and Edge | `Cannot read properties of undefined (reading 'grand')` | | Firefox | `can't access property "grand", order.totals is undefined` | | Safari | `undefined is not an object (evaluating 'order.totals.grand')` | If the message says `of null` instead, the value was `null`. That's often a DOM lookup, because `document.querySelector` and `getElementById` return `null` when nothing matches. ## Why it happens in production On your machine it works. In a customer's browser it doesn't. When this error arrives in a bug report, it's almost always one of four things. ### The data hadn't loaded yet The code ran before the thing it needed arrived. A component renders before its fetch resolves, or a click handler fires while a request is still in flight. Your laptop on office wifi wins that race every time. A customer on a train loses it. ### The API sent a different shape The code expected `{ totals: { grand: 12.5 } }` and got something else. A backend change renamed a field, an optional field was missing for this one customer, or the request failed and the code parsed the error body as if it were the order. That last one is common, because `fetch` doesn't throw on a 401 or a 500. It resolves, `res.json()` happily returns `{ "message": "Unauthenticated" }`, and the next line crashes. ### A DOM element wasn't on the page `document.getElementsByClassName('promo-code')[0].value` works until a customer gets the page without a promo field: a logged-out layout, an A/B test variant, a feature flag. The collection is empty, `[0]` is undefined, and reading `.value` throws. ### The page came from an older deploy Someone opened your app yesterday and never reloaded. Their tab is still running last week's JavaScript against today's API, or today's code is reading something an older version saved to `localStorage` in a different shape. These are the reports nobody can reproduce, because a fresh page load fixes them. ## How to tell which cause it is Don't start with a fix. Start with the evidence, because each cause leaves a different trail. | What the report shows | What it points to | | --- | --- | | The error came seconds after page load or a click, with the request still pending or just finished | Data not loaded yet | | A failed request (401, 500) or a response missing the field, just before the error | The API shape | | The [stack trace](/guides/what-is-a-stack-trace) lands on a DOM lookup, and the customer was on an unusual page or variant | A missing element | | The tab had been open for hours or days, or the frames point at a bundle file you no longer serve | An older deploy | Start with the top frame in your own code. A production trace is minified, so you'll need a [source map](/guides/what-is-a-source-map) to read it. Then look at the network requests just before the error. If the order request returned a 401, you already have your answer, and the fix belongs in the error handling, not the rendering. Finally, read what the customer did. "I left it open over lunch and came back" tells you more than any amount of code reading. If the problem was left as a Snags comment, the console error, the failed requests and the steps before it arrive together in the comment ([what gets captured](/docs/what-gets-captured)). Otherwise, ask for a [HAR file](/guides/what-is-a-har-file) and the time it happened. ## How to fix it Optional chaining (`?.`) is the fix everyone reaches for, and it's right when a value is genuinely allowed to be missing. Used everywhere, it just turns a loud crash into a silent wrong answer: a total that reads as blank, a button that does nothing. I'd rather the page fail loudly with a clear message than quietly show a customer the wrong price. ```js // Allowed to be missing: say so, and give a default. const coupon = order.discount?.code ?? 'none'; // Never allowed to be missing: fail early, with a message that names the cause. async function loadOrder(id) { const res = await fetch(`/api/orders/${id}`); if (!res.ok) throw new Error(`GET /api/orders/${id} returned ${res.status}`); const order = await res.json(); if (!order.totals) throw new Error(`Order ${id} arrived without totals`); return order; } ``` For data that hasn't loaded, render a loading state instead of guarding every property: ```jsx import { useEffect, useState } from 'react'; function CartTotal({ id }) { const [order, setOrder] = useState(null); const [error, setError] = useState(null); useEffect(() => { loadOrder(id).then(setOrder, setError); }, [id]); if (error) return
We couldn't load your cart.
; if (!order) returnLoading your cart…
; returnTotal: {order.totals.grand.toFixed(2)}
; } ``` For missing elements, check the lookup before you use it. For old deploys, version anything you save to `localStorage` and throw away data from a version you don't recognise. ## For support teams When a customer hits this, ask what they clicked and how long the tab had been open. Ask them to reload too. If a reload fixes it, tell engineering, because it points at an older deploy rather than at their account. --- Source: https://snags.com/guides/failed-to-fetch # TypeError: Failed to fetch: what it means and how to find the cause > Failed to fetch means a fetch() request never got an HTTP response. The six usual causes, and how to tell them apart using the console and the Network tab. "Failed to fetch" is the TypeError Chrome and Edge throw when a `fetch()` request never gets a usable HTTP response, because it was blocked, cancelled or couldn't connect. It isn't a status code, so a 404 or a 500 never causes it. Firefox reports the same failure as "NetworkError when attempting to fetch resource", and Safari says "Load failed". ## Why the error message doesn't tell you the cause Every cause of a failed fetch produces the same TypeError in your code, on purpose. If JavaScript could tell a CORS rejection from a refused connection, any website could use your browser to probe servers it isn't allowed to read, including ones on your private network. So the page gets one vague error, and the real reason goes somewhere only a person can see it. Here's what Chrome gave the page's own code for four very different failures: ```text CORS rejection TypeError: Failed to fetch Unknown host TypeError: Failed to fetch Connection refused TypeError: Failed to fetch Mistyped URL scheme TypeError: Failed to fetch ``` And here's what it printed in the console at the same moment, with the addresses shortened: ```text Access to fetch at 'https://api.example.com/orders' from origin 'https://shop.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Failed to load resource: net::ERR_NAME_NOT_RESOLVED Failed to load resource: net::ERR_CONNECTION_REFUSED Fetch API cannot load htp://example.com/. URL scheme "htp" is not supported. ``` That's the whole problem in two code blocks. The answer is always in the console or the Network tab, never in `error.message`. Which also means an error tracker that only stores the message is storing "Failed to fetch" a thousand times and telling you nothing. Log the URL and method alongside it at the very least. ## The causes, and how to recognise each one Six causes cover nearly every case. The console lines are Chrome's, and Edge prints the same ones. Firefox words them differently and, confusingly, blames CORS for some failures that have nothing to do with it: an unknown host comes out as "CORS request did not succeed". | Cause | What Chrome's console says | What to check | | --- | --- | --- | | CORS | `has been blocked by CORS policy` and the missing header | The API's response headers, for this origin | | Offline, VPN or DNS | `net::ERR_INTERNET_DISCONNECTED` or `net::ERR_NAME_NOT_RESOLVED` | Whether the host resolves from that network | | Blocked by an extension | `net::ERR_BLOCKED_BY_CLIENT` | Retry in a private window with extensions off | | Navigation cancelled it | Often nothing. The Network tab shows the request as `(canceled)` | A link or form that left the page mid-request | | Mixed content | `Mixed Content: The page at '…' was loaded over HTTPS, but requested an insecure resource` | An `http://` URL on an `https://` page | | Bad URL or port | `net::ERR_CONNECTION_REFUSED`, or `URL scheme "…" is not supported` | Typos, a missing `https://`, a local API that isn't running | CORS is the one that wastes the most time, because it looks like a network error and is really a server configuration. The request often reached your API and got a perfectly good answer. The browser just refused to hand it to the page. `curl` ignores CORS, which makes it the quickest test: if curl gets a 200 and the browser says Failed to fetch, the request works and the headers don't. Extensions are the one support teams should suspect first for a single customer. Ad and privacy blockers match on URL patterns, so an endpoint called `/api/track-order` or `/analytics/summary` can be blocked for that customer and fine for everyone else. Cancelled navigation catches out a lot of forms. A form whose submit handler calls `fetch` but never calls `event.preventDefault()` starts the request, then the browser reloads the page and kills it. The console shows Failed to fetch for a split second, if you catch it at all. ## How to find the cause step by step 1. Open the developer tools, switch to the Network tab, and tick Preserve log so a page change doesn't wipe the evidence. 2. Reproduce the error, then find the red request. Its status column says `CORS error`, `(blocked:mixed-content)`, `(canceled)` or `(failed)`. 3. Read the matching line in the console. It names the reason the page couldn't see. 4. Try the same URL with curl, and in a private window. That separates the server, the browser and the extensions in two goes. To check CORS from the command line, send the preflight request the browser would send: ```bash curl -i -X OPTIONS https://api.example.com/orders \ -H "Origin: https://shop.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type" ``` The response needs an `Access-Control-Allow-Origin` header naming your origin (or `*`, if the request sends no cookies) and an `Access-Control-Allow-Headers` header that includes `content-type`. If either is missing, the fix is on the API. ## When the error comes from a customer You can't open the console on someone else's computer, and the customer usually can't tell you what it says. Ask which browser and extensions they use, and whether they're on a VPN or a work network. Then ask for a [HAR file](/guides/what-is-a-har-file), which records every request including the failed one. The [HAR checker](/tools/har-checker) reads one and strips the secrets before you share it. If you'd like to try the console yourself first, [here's how to open it](/guides/open-the-console) in every browser. ## Handling it in your code Catch it, keep the context, and don't pretend it's always the customer's connection: ```js async function getJSON(url, options) { let res; try { res = await fetch(url, options); } catch (err) { if (err.name === 'AbortError') throw err; // cancelled on purpose throw new Error(`Request to ${url} failed before a response arrived`, { cause: err }); } if (!res.ok) throw new Error(`${url} returned ${res.status}`); return res.json(); } ``` A "check your internet connection" message for every Failed to fetch is often simply wrong. `navigator.onLine === false` reliably means offline, but `true` only means there's a network interface, not that your API is reachable. Show the connection message when you know, and a plain "something went wrong, try again" when you don't. ## For support teams Ask the customer to try once in a private window. If that works, an extension is the likely cause, and it's worth naming it in the ticket so engineering can rename the blocked endpoint rather than chase a bug that only exists in one browser. --- Source: https://snags.com/guides/hard-refresh # How to hard refresh in Chrome, Edge, Firefox and Safari > A hard refresh reloads a page without your browser's saved copies. The keys for Chrome, Edge, Firefox and Safari on Windows and Mac, and when to clear cache. A hard refresh reloads the page you're on and ignores the copies of its files that your browser saved on earlier visits, so you get what the website is serving right now. In Chrome, Edge and Firefox, press Ctrl+Shift+R on Windows or Cmd+Shift+R on a Mac. In Safari, press Option+Cmd+R. ## Hard refresh shortcuts for every browser Every shortcut below comes from the browser maker's own keyboard shortcut list, except Safari's, which Apple doesn't list anywhere. | Browser | Windows | Mac | | --- | --- | --- | | Chrome | Ctrl+Shift+R or Shift+F5 | Cmd+Shift+R | | Edge | Ctrl+Shift+R or Shift+F5 | Cmd+Shift+R | | Firefox | Ctrl+Shift+R or Ctrl+F5 | Cmd+Shift+R | | Safari | Not available on Windows | Option+Cmd+R | On a Mac, Cmd+Shift+R works in Chrome, Edge and Firefox. Safari is the odd one out, and it catches a lot of people. In Safari, Cmd+Shift+R opens Reader, the stripped-down reading view, so it looks like something happened when nothing was reloaded at all. Safari calls its hard refresh Reload Page From Origin. It's Option+Cmd+R, and you'll find it in the View menu if you hold down Option while the menu is open. A plain refresh (F5, Ctrl+R or Cmd+R) is not the same thing. The browser is still allowed to reuse files it already has, which is exactly what you're trying to avoid. ## How to hard refresh on a phone Phones don't have one. Chrome on Android and Safari on iPhone offer no shortcut or menu item for a hard refresh, so the closest you can get is clearing the cache, which the next section covers. Pull-to-refresh is an ordinary reload. ## What does a hard refresh actually do? A hard refresh reloads the page and asks the website for every file again, instead of using the copies in your browser's cache. It also skips the site's service worker, the background script some web apps use to serve pages offline. It doesn't touch your cookies, saved logins or anything a site stored about you. That last part matters more than people expect. The cache holds the page's code, styles and images. When a site ships an update and your browser keeps serving last week's JavaScript next to this week's HTML, things break in odd ways: buttons that do nothing, a layout that's half old and half new, a "something went wrong" that nobody else can reproduce. A hard refresh fixes that, and it's the first thing I'd try for any "it looks wrong" problem. It does nothing for problems that live in your account or your session. If you're stuck in a login loop or getting an error that names your account, the cause is somewhere a hard refresh can't reach. Doing it five times won't help. Clearing cookies, or a teammate fixing something on the server, will. ## When a hard refresh isn't enough, clear the cache Clearing the cache deletes every file your browser saved from websites, for the time range you choose, so every site loads fresh next time. It's the bigger hammer, and the one to reach for when a hard refresh didn't help or you're on a phone. The trap is the checkbox next to it. Clearing "cookies and other site data" signs you out of websites, and many people tick everything because it's quicker. If you only want a fresh copy of the site, tick cached images and files and leave cookies alone. | Browser | How to open it | What to tick | | --- | --- | --- | | Chrome | Ctrl+Shift+Delete on Windows, Cmd+Shift+Delete on Mac | Cached images and files | | Edge | Ctrl+Shift+Delete on Windows, Cmd+Shift+Delete on Mac | Cached images and files | | Firefox | Ctrl+Shift+Delete on Windows, Cmd+Shift+Delete on Mac | The cache option only | | Safari on Mac | Turn on Settings, Advanced, "Show features for web developers", then Develop, Empty Caches | Nothing to tick, it only empties caches | | Chrome on Android | More, then Delete browsing data, then More options | Cached images and files | | Safari on iPhone | Settings, Apps, Safari, Advanced, Website Data, Remove All Website Data | This removes cookies too, so expect to sign in again | Pick a time range that covers the problem. "Last hour" is often enough if the site broke today, and it keeps the rest of your browsing fast. ## Hard refresh, clearing the cache and clearing cookies compared The three get used interchangeably in support replies, and they do very different things. | | Hard refresh | Clear the cache | Clear cookies | | --- | --- | --- | --- | | Which sites | The page you're on | Every site, for the time range | Every site, for the time range | | Fetches the site's files again | Yes | Yes | No | | Signs you out | No | No | Yes | | Good for | A page that looks broken after an update | A hard refresh that didn't fix it | Login loops and account-specific errors | My rule of thumb is to work down the table one column at a time and stop as soon as the problem is gone. ## For support teams Give the exact keys for the customer's browser and system, not just "do a hard refresh". When a Mac user says "I tried that" and nothing changed, check they weren't in Safari pressing Cmd+Shift+R. If you don't know which browser they use, the [Browser info tool](/tools/browser-info) tells you in one paste. And if you ask for a cache clear, tell them which box to tick, or they'll clear their cookies too and come back signed out of everything. --- Source: https://snags.com/guides/what-is-a-user-agent # What is a user agent? How to read a user agent string > A user agent string tells websites which browser and system you use. How to read one part by part, why it misleads, and what Client Hints change. A user agent is the program acting for you on the web, which almost always means your browser. With every request, it sends a line of text called the user agent string that names the browser, its version and your operating system. Sites use it for statistics, support and, less wisely, for deciding what to show you. ## What is a user agent string? A user agent string is the value of the `User-Agent` header that a browser sends with every request, and the same text JavaScript reads from `navigator.userAgent`. It names the browser, its rendering engine and the operating system it's running on, in a format that grew by accident over thirty years and now mostly exists for compatibility. Here's the one Chrome 155 sends from a Mac: ```text Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 ``` It mentions four browsers and two engines that aren't Chrome. That's normal. ## How to read a user agent string Read it left to right as a list of products, each one a name, a slash and a version, with comments in brackets. Most of the tokens are fossils from the browser wars that every browser keeps sending so old sites don't lock them out. Only one or two parts actually identify the browser. | Part | What it claims | What it really tells you | | --- | --- | --- | | `Mozilla/5.0` | A Mozilla-compatible browser | Nothing. Almost every browser sends it, for historical reasons | | `(Macintosh; Intel Mac OS X 10_15_7)` | An Intel Mac on macOS 10.15.7 | A Mac. Chrome sends this fixed value whatever the chip or macOS version | | `AppleWebKit/537.36` | Apple's WebKit engine | Chrome's engine was forked from WebKit, and the number stopped moving long ago | | `(KHTML, like Gecko)` | KDE's engine, behaving like Firefox's | Nothing. Another compatibility fossil | | `Chrome/155.0.0.0` | Chrome, version 155 | The real browser and its major version. The zeros are deliberate | | `Safari/537.36` | Safari | Nothing. It's there so sites that check for Safari let Chrome in | The rule I use is to read from the right. Browsers add their own name at the end, so the last product token is usually the honest one. Edge sends the same string as Chrome with `Edg/155.0.0.0` on the end. Firefox's is shorter and ends in `Firefox/` and a version, after a `Gecko/20100101` that has been frozen at that date for years. ```text Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0 ``` ## Why the user agent is unreliable for browser detection A user agent string is a claim the browser makes, not a fact you can check. People change it with an extension or a developer tools setting, browsers fake parts of it on purpose, and the version numbers that would make it useful have been frozen. Treat it as a hint for logs and support, never as the thing that decides which code runs. Some of the lying is built in. Windows 11 still reports `Windows NT 10.0`, and Microsoft has said the string won't be updated to tell the two apart. That Mac string above is the same on a brand-new Apple Silicon machine as on a 2019 Intel one. Every browser claims to be Mozilla, Chrome claims to be Safari, and Edge claims to be Chrome. Then there's the code people write on top of it. Before Chrome reached version 100 in 2022, Google spent months warning that user agent parsers expecting a two-digit version would break, and handed developers a flag to test their sites against it. Nothing about the browser had changed except one number, and that was enough to break the parsers. If you need to know whether a browser can do something, check for the feature itself (`'share' in navigator`, `CSS.supports(...)`) and let the user agent go back to being a label. Apple's Safari documentation says the same thing. ## What is user-agent reduction? User-agent reduction is Chrome's change, now complete, that cut the user agent string down to a fixed shape: the browser's major version, a frozen operating system value and zeros for the minor version. The aim was to make the string less useful for fingerprinting people. The details moved to Client Hints, which sites have to ask for. It happened in stages. Chrome 101 replaced the minor version with zeros. Chrome 107 froze the desktop operating system and CPU details to one value per platform, which is why every Mac running Chrome reports `Intel Mac OS X 10_15_7` and every Windows PC reports `Windows NT 10.0; Win64; x64`. Chrome 110 started doing the same on Android, where the string now says `Android 10; K` whatever the phone is. Google's own documentation now describes the reduction as complete, and it covers what JavaScript sees in `navigator.userAgent`, `navigator.appVersion` and `navigator.platform` as well as the header. ## User-Agent Client Hints Client Hints are the replacement. By default Chrome and Edge send three extra headers with every HTTPS request: `Sec-CH-UA` (brands and major version), `Sec-CH-UA-Mobile` and `Sec-CH-UA-Platform`. Anything more specific, like the full version, the operating system version or the device model, has to be requested. A server asks with an `Accept-CH` response header, and a page asks from JavaScript: ```js const data = await navigator.userAgentData?.getHighEntropyValues([ "platformVersion", "fullVersionList", "model", ]); console.log(data ?? navigator.userAgent); ``` That's the way to tell Windows 11 from Windows 10: a `platformVersion` starting at 13 or above is Windows 11. Two caveats keep this from being a clean replacement. Client Hints only exist in Chromium browsers, and as of October 2026 neither Firefox nor Safari supports them. And the browser decides what to hand back, so a high-entropy value can still come back empty. ## For support teams The user agent your customer copies for you is a good start, and it will tell you the browser and its major version. It won't tell you the exact Chrome build, whether they're on Windows 10 or 11, or which Mac they own. If those matter for the bug, ask for them directly, or send them to the [Browser info tool](/tools/browser-info), which collects the details a support reply usually needs and copies them in one go. --- Source: https://snags.com/guides/bug-report-template # The bug report template that actually gets bugs fixed > A copy-paste bug report template with a filled-in example, GitHub and Jira variants, and honest notes on which fields matter and which are theater. A good bug report answers four questions without a follow-up call: what did you do, what did you expect, what happened instead, and where's the evidence. Every field in the template below exists to answer one of those. Copy it as is, or take the GitHub and Jira variants further down. ## The template ```markdown ## Title One sentence describing the symptom, not the suspected cause. ## Environment - Browser and version: - OS and device: - Deployed release or commit: - Account / role (if relevant): ## Steps to reproduce 1. Start from a fresh session at