diff --git a/.github/workflows/screenshot-frontmatter.yml b/.github/workflows/screenshot-frontmatter.yml
new file mode 100644
index 0000000000000..d19d8fcab421b
--- /dev/null
+++ b/.github/workflows/screenshot-frontmatter.yml
@@ -0,0 +1,115 @@
+name: Auto-add sentry_ui_url
+
+# Automatically adds sentry_ui_url to MDX frontmatter when a PR
+# adds or modifies image references in docs pages. This ensures
+# the screenshot pipeline can always find the right Sentry UI page
+# to capture, without writers having to remember a manual step.
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ paths:
+ - 'docs/**/*.mdx'
+ - 'docs/**/*.md'
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ add-screenshot-urls:
+ runs-on: ubuntu-latest
+ # Don't run on PRs from the screenshot pipeline itself
+ if: ${{ !startsWith(github.head_ref, 'docs/auto-screenshot-update') }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - uses: pnpm/action-setup@v2
+
+ - name: Install root dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Check for missing sentry_ui_url
+ id: check
+ run: |
+ # Get list of changed MDX files in this PR
+ CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- 'docs/**/*.mdx' 'docs/**/*.md')
+
+ if [ -z "$CHANGED_FILES" ]; then
+ echo "No changed MDX files found"
+ echo "needs_fix=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ # Check each changed file for images without sentry_ui_url
+ MISSING_FILES=""
+ for file in $CHANGED_FILES; do
+ if [ ! -f "$file" ]; then
+ continue
+ fi
+
+ # Check if file has image references (skip external URLs)
+ HAS_IMAGES=$(grep -cE '!\[[^\]]*\]\([^)]+\.(png|jpg|jpeg|gif|webp)\)' "$file" 2>/dev/null || true)
+ if [ "$HAS_IMAGES" -eq "0" ] 2>/dev/null; then
+ continue
+ fi
+
+ # Check if frontmatter has sentry_ui_url
+ HAS_URL=$(head -30 "$file" | grep -c 'sentry_ui_url:' || true)
+ if [ "$HAS_URL" -eq "0" ]; then
+ MISSING_FILES="$MISSING_FILES $file"
+ fi
+ done
+
+ if [ -z "$MISSING_FILES" ]; then
+ echo "All changed files with images have sentry_ui_url"
+ echo "needs_fix=false" >> $GITHUB_OUTPUT
+ else
+ echo "Files missing sentry_ui_url:$MISSING_FILES"
+ echo "needs_fix=true" >> $GITHUB_OUTPUT
+ echo "missing_files=$MISSING_FILES" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Install pipeline dependencies
+ if: steps.check.outputs.needs_fix == 'true'
+ run: npm install
+ working-directory: scripts/screenshot-pipeline
+
+ - name: Run auto-map and inject
+ if: steps.check.outputs.needs_fix == 'true'
+ run: |
+ # Run the crawler to build manifest
+ npx ts-node src/crawl-inventory.ts --scope docs/product
+ # Run auto-mapper
+ npx ts-node tools/auto-map-urls.ts
+ # Inject URLs into frontmatter
+ npx ts-node tools/inject-frontmatter-urls.ts
+ working-directory: scripts/screenshot-pipeline
+ env:
+ UI_REFRESH_CUTOFF: '2025-06-01'
+
+ - name: Commit changes
+ if: steps.check.outputs.needs_fix == 'true'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ # Only commit the MDX files that were changed (not pipeline output)
+ git add 'docs/**/*.mdx' 'docs/**/*.md'
+
+ # Check if there are actual changes
+ if git diff --cached --quiet; then
+ echo "No frontmatter changes to commit"
+ else
+ git commit -m "chore(docs): auto-add sentry_ui_url to screenshot pages"
+ git push
+ echo "Committed sentry_ui_url additions"
+ fi
diff --git a/.github/workflows/screenshot-pipeline.yml b/.github/workflows/screenshot-pipeline.yml
new file mode 100644
index 0000000000000..40a25ca73ac48
--- /dev/null
+++ b/.github/workflows/screenshot-pipeline.yml
@@ -0,0 +1,152 @@
+name: Screenshot Pipeline
+
+# Automated screenshot capture, diff, and replacement pipeline.
+# Detects stale screenshots and Arcade embeds, auto-replaces high-confidence
+# diffs, and creates Linear issues for items requiring manual review.
+
+on:
+ schedule:
+ - cron: '0 6 * * 1' # Weekly on Monday at 6am UTC
+ workflow_dispatch: # Manual trigger
+ inputs:
+ scope:
+ description: 'Limit crawl to a specific directory (e.g., docs/product/issues)'
+ required: false
+ type: string
+ dry_run:
+ description: 'Dry run mode (no file changes, no PRs, no Linear issues)'
+ required: false
+ type: boolean
+ default: false
+ diff_threshold_low:
+ description: 'Min diff % to consider changed (default: 0.01 = 1%)'
+ required: false
+ type: string
+ default: '0.01'
+ diff_threshold_high:
+ description: 'Max diff % before flagging as suspicious (default: 0.50 = 50%)'
+ required: false
+ type: string
+ default: '0.50'
+
+jobs:
+ screenshot-pipeline:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history for git log queries
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install pipeline dependencies
+ run: npm install
+ working-directory: scripts/screenshot-pipeline
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+ working-directory: scripts/screenshot-pipeline
+
+ - name: Restore auth state
+ if: ${{ !inputs.dry_run }}
+ run: |
+ echo "$SENTRY_STORAGE_STATE" | base64 -d > /tmp/storageState.json
+ env:
+ SENTRY_STORAGE_STATE: ${{ secrets.SENTRY_STORAGE_STATE }}
+
+ - name: Validate scope input
+ if: ${{ inputs.scope != '' }}
+ run: |
+ # Validate scope is a safe directory path (alphanumeric, hyphens, slashes, dots only)
+ if ! echo "$PIPELINE_SCOPE" | grep -qE '^[a-zA-Z0-9/_.-]+$'; then
+ echo "Error: Invalid scope input. Must be a directory path (e.g., docs/product/issues)"
+ exit 1
+ fi
+ env:
+ PIPELINE_SCOPE: ${{ inputs.scope }}
+
+ - name: Run inventory crawler
+ run: |
+ SCOPE_ARGS=()
+ if [ -n "$PIPELINE_SCOPE" ]; then
+ SCOPE_ARGS=(--scope "$PIPELINE_SCOPE")
+ fi
+ npx ts-node src/crawl-inventory.ts "${SCOPE_ARGS[@]}"
+ working-directory: scripts/screenshot-pipeline
+ env:
+ UI_REFRESH_CUTOFF: '2025-06-01'
+ PIPELINE_SCOPE: ${{ inputs.scope }}
+
+ - name: Run screenshot capture & diff
+ run: |
+ DRY_RUN_ARGS=()
+ if [ "$PIPELINE_DRY_RUN" = "true" ]; then
+ DRY_RUN_ARGS=(--dry-run)
+ fi
+ npx ts-node src/capture-and-diff.ts "${DRY_RUN_ARGS[@]}"
+ working-directory: scripts/screenshot-pipeline
+ env:
+ SENTRY_STORAGE_STATE_PATH: ${{ inputs.dry_run && '' || '/tmp/storageState.json' }}
+ SENTRY_ORG_SLUG: ${{ secrets.SENTRY_ORG_SLUG }}
+ SENTRY_BASE_URL: ${{ secrets.SENTRY_BASE_URL }}
+ DIFF_THRESHOLD_LOW: ${{ inputs.diff_threshold_low || '0.01' }}
+ DIFF_THRESHOLD_HIGH: ${{ inputs.diff_threshold_high || '0.50' }}
+ PIPELINE_DRY_RUN: ${{ inputs.dry_run }}
+
+ - name: Auto-replace and open PR
+ if: ${{ !inputs.dry_run }}
+ run: npx ts-node src/auto-replace.ts
+ working-directory: scripts/screenshot-pipeline
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create Linear issues
+ if: ${{ !inputs.dry_run }}
+ run: npx ts-node src/create-linear-issues.ts
+ working-directory: scripts/screenshot-pipeline
+ env:
+ LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
+ LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID }}
+ SENTRY_BASE_URL: ${{ secrets.SENTRY_BASE_URL }}
+
+ - name: Upload pipeline artifacts
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: screenshot-pipeline-output
+ path: scripts/screenshot-pipeline/output/
+ retention-days: 30
+
+ - name: Write job summary
+ if: always()
+ run: |
+ echo "## Screenshot Pipeline Results" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ if [ -f scripts/screenshot-pipeline/output/inventory-manifest.json ]; then
+ TOTAL=$(jq length scripts/screenshot-pipeline/output/inventory-manifest.json)
+ STALE=$(jq '[.[] | select(.is_stale == true)] | length' scripts/screenshot-pipeline/output/inventory-manifest.json)
+ echo "**Inventory:** ${TOTAL} total assets, ${STALE} stale" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [ -f scripts/screenshot-pipeline/output/diff-results.json ]; then
+ CAPTURED=$(jq length scripts/screenshot-pipeline/output/diff-results.json)
+ AUTO=$(jq '[.[] | select(.status == "auto_replace")] | length' scripts/screenshot-pipeline/output/diff-results.json)
+ REVIEW=$(jq '[.[] | select(.status == "needs_review")] | length' scripts/screenshot-pipeline/output/diff-results.json)
+ FAILED=$(jq '[.[] | select(.status == "capture_failed")] | length' scripts/screenshot-pipeline/output/diff-results.json)
+ echo "**Captures:** ${CAPTURED} processed" >> $GITHUB_STEP_SUMMARY
+ echo "- Auto-replace: ${AUTO}" >> $GITHUB_STEP_SUMMARY
+ echo "- Needs review: ${REVIEW}" >> $GITHUB_STEP_SUMMARY
+ echo "- Capture failed: ${FAILED}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [ -f scripts/screenshot-pipeline/output/linear-issues.json ]; then
+ CREATED=$(jq '[.[] | select(.isNew == true)] | length' scripts/screenshot-pipeline/output/linear-issues.json)
+ echo "**Linear issues created:** ${CREATED}" >> $GITHUB_STEP_SUMMARY
+ fi
diff --git a/.gitignore b/.gitignore
index b51a5640d2c94..651b71581fe4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,3 +109,7 @@ yalc.lock
.claude/settings.local.json
mise.toml
.yarn/install-state.gz
+
+# Screenshot pipeline output (generated at runtime)
+scripts/screenshot-pipeline/output/
+scripts/screenshot-pipeline/node_modules/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d7943a39b3d81..366befffb6888 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,3 +21,24 @@ pnpm dev:developer-docs
With that, the repo is fully set up and you are ready to open local docs under http://localhost:3000
`next-env.d.ts` is in `.gitignore` and is generated when you run `pnpm dev` or `pnpm build`. When we upgrade to Next 15.5+, we can run `next typegen` in CI and in `lint:ts` so the file is generated before type-check.
+
+## Screenshots
+
+Docs pages that contain screenshots of the Sentry UI use a `sentry_ui_url` field in their frontmatter. This tells the automated screenshot pipeline which Sentry page to capture when checking for stale images.
+
+```yaml
+---
+title: Issue Details
+sidebar_order: 10
+description: Learn how to navigate the Issue Details page.
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
+---
+```
+
+Use `{org}` as a placeholder for the organization slug. The pipeline replaces it at capture time.
+
+**This field is added automatically.** When you open a PR that adds or modifies images in a docs page, a GitHub Actions workflow will detect the missing field and commit it to your branch. You can also add it manually if you know the URL.
+
+If the auto-detected URL is wrong (e.g., your screenshots show a specific sub-page), update it in the frontmatter and the bot won't overwrite it on future PRs.
+
+See [`scripts/screenshot-pipeline/README.md`](scripts/screenshot-pipeline/README.md) for full pipeline documentation.
diff --git a/docs/product/dashboards/sentry-dashboards/mobile/index.mdx b/docs/product/dashboards/sentry-dashboards/mobile/index.mdx
index 2ffb6eb796ced..5d62f1647d445 100644
--- a/docs/product/dashboards/sentry-dashboards/mobile/index.mdx
+++ b/docs/product/dashboards/sentry-dashboards/mobile/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to use Sentry's Mobile Performance dashboard to monitor the health of
your mobile app, including app starts, screen loads, and screen rendering.
og_image: /og-images/product-insights-mobile.png
+sentry_ui_url: https://sentry.io/organizations/{org}/dashboards/
---
The Mobile Dashboard found on [Sentry Dashboards](https://sentry.io/orgredirect/organizations/:orgslug/dashboards/) gives an overview of the metrics that let you know how fast your app starts, including the number of slow and frozen frames your users may be experiencing. Each metric provides insights into the overall performance health of your mobile application. Digging into the details helps prioritize critical performance issues and allows you to identify and troubleshoot the root cause faster.
diff --git a/docs/product/dashboards/sentry-dashboards/mobile/mobile-vitals/screen-loads.mdx b/docs/product/dashboards/sentry-dashboards/mobile/mobile-vitals/screen-loads.mdx
index a5259bf9a396b..427a169c18210 100644
--- a/docs/product/dashboards/sentry-dashboards/mobile/mobile-vitals/screen-loads.mdx
+++ b/docs/product/dashboards/sentry-dashboards/mobile/mobile-vitals/screen-loads.mdx
@@ -6,6 +6,7 @@ description: >-
Loads to get better visibility on your application's TTID and TTFD
performance.
og_image: /og-images/product-insights-mobile-mobile-vitals-screen-loads.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/traces/
---
{/**/}
diff --git a/docs/product/dashboards/widget-builder/index.mdx b/docs/product/dashboards/widget-builder/index.mdx
index 6346bfa232374..d1a42a01d1bdd 100644
--- a/docs/product/dashboards/widget-builder/index.mdx
+++ b/docs/product/dashboards/widget-builder/index.mdx
@@ -3,6 +3,7 @@ title: Widget Builder
sidebar_order: 15
description: Learn how to create widgets for your dashboards or edit existing ones.
og_image: /og-images/product-dashboards-widget-builder.png
+sentry_ui_url: https://sentry.io/organizations/{org}/dashboards/new/
---
When adding a widget or editing an existing one, a side panel opens up where you can see the dashboard widget builder.
diff --git a/docs/product/explore/discover-queries/index.mdx b/docs/product/explore/discover-queries/index.mdx
index 53e29ee8f3d0a..7f51bffe370c4 100644
--- a/docs/product/explore/discover-queries/index.mdx
+++ b/docs/product/explore/discover-queries/index.mdx
@@ -5,6 +5,7 @@ description: >-
Discover is a powerful query engine that allows you to query all your metadata
across projects and environments to unlock insights.
og_image: /og-images/product-explore-discover-queries.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/discover/homepage/
---
The Discover homepage displays the query builder where you can immediately begin creating a new query. By default, the page shows the pre-built "All Events" query, but you can customize the homepage and [set the custom version as your default](#set-as-default). To navigate back to the query management page, where you can find [pre-built](#pre-built-queries) and [saved](#saved-queries) queries, click the "Saved Queries" button.
diff --git a/docs/product/explore/discover-queries/query-builder.mdx b/docs/product/explore/discover-queries/query-builder.mdx
index 88586be87c13e..5b6fcbf62a2ef 100644
--- a/docs/product/explore/discover-queries/query-builder.mdx
+++ b/docs/product/explore/discover-queries/query-builder.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to use Discover to build a query, filter your data, and group
results.
og_image: /og-images/product-explore-discover-queries-query-builder.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/discover/homepage/
---
diff --git a/docs/product/explore/discover-queries/query-builder/query-equations.mdx b/docs/product/explore/discover-queries/query-builder/query-equations.mdx
index 6cb63dc3e7b57..f4e94d0e6466a 100644
--- a/docs/product/explore/discover-queries/query-builder/query-equations.mdx
+++ b/docs/product/explore/discover-queries/query-builder/query-equations.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to write query equations that perform operations using your query
columns.
og_image: /og-images/product-explore-discover-queries-query-builder-query-equations.gif
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/discover/homepage/
---
diff --git a/docs/product/explore/discover-queries/uncover-trends.mdx b/docs/product/explore/discover-queries/uncover-trends.mdx
index 42d16313bac62..ce8d3bfde73f0 100644
--- a/docs/product/explore/discover-queries/uncover-trends.mdx
+++ b/docs/product/explore/discover-queries/uncover-trends.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to work with the data in Discover to gain useful insights into the
health and stability of your applications.
og_image: /og-images/product-explore-discover-queries-uncover-trends.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/discover/homepage/
---
diff --git a/docs/product/explore/logs/index.mdx b/docs/product/explore/logs/index.mdx
index 19bd1129935fe..872df9b10534c 100644
--- a/docs/product/explore/logs/index.mdx
+++ b/docs/product/explore/logs/index.mdx
@@ -5,6 +5,7 @@ description: >-
Structured logs allow you to send, view and query logs and parameters sent
from your applications within Sentry.
og_image: /og-images/product-explore-logs.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/logs/
---
diff --git a/docs/product/explore/profiling/differential-flamegraphs.mdx b/docs/product/explore/profiling/differential-flamegraphs.mdx
index e57d08e4acc71..3ade8b905f762 100644
--- a/docs/product/explore/profiling/differential-flamegraphs.mdx
+++ b/docs/product/explore/profiling/differential-flamegraphs.mdx
@@ -3,6 +3,7 @@ title: Differential Flame Graphs
sidebar_order: 110
description: Learn how to use and interpret differential flame graphs.
og_image: /og-images/product-explore-profiling-differential-flamegraphs.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/profiling/
---
There are multiple ways to visualize and use profiling data, including differential flame graphs, which can help easily identify function regressions or changes in the execution context.
diff --git a/docs/product/explore/profiling/flame-charts-graphs.mdx b/docs/product/explore/profiling/flame-charts-graphs.mdx
index 1313f16486e86..86d84410636b4 100644
--- a/docs/product/explore/profiling/flame-charts-graphs.mdx
+++ b/docs/product/explore/profiling/flame-charts-graphs.mdx
@@ -3,6 +3,7 @@ title: Flame Graphs and Aggregated Flame Graphs
sidebar_order: 100
description: Learn more about how to interpret flame graphs and aggregated flame graphs.
og_image: /og-images/product-explore-profiling-flame-charts-graphs.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/profiling/
---
Profiling data can be used to gain insight into what methods and lines of your code are slow. But getting this type of insight requires an understanding of how profiling data is represented and visualized. Sentry uses both flame graphs and aggregated flame graphs to visualize profile data. We'll explain how to read them below.
diff --git a/docs/product/explore/profiling/index.mdx b/docs/product/explore/profiling/index.mdx
index 2d3c54fd89f78..4d7b5c40d502c 100644
--- a/docs/product/explore/profiling/index.mdx
+++ b/docs/product/explore/profiling/index.mdx
@@ -6,6 +6,7 @@ description: >-
removing the need for custom instrumentation and enabling precise code-level
visibility into your application in a production environment.
og_image: /og-images/product-explore-profiling.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/profiling/
---
Sentry’s Profiling products provide precise, code-level visibility into application execution in a production environment.
diff --git a/docs/product/explore/profiling/mobile-app-profiling/metrics.mdx b/docs/product/explore/profiling/mobile-app-profiling/metrics.mdx
index c27d3bd8cf2d6..7e265857ef0be 100644
--- a/docs/product/explore/profiling/mobile-app-profiling/metrics.mdx
+++ b/docs/product/explore/profiling/mobile-app-profiling/metrics.mdx
@@ -6,6 +6,7 @@ description: >-
contextualize the work your mobile app does and detect possible issues
impacting performance.
og_image: /og-images/product-explore-profiling-mobile-app-profiling-metrics.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/profiling/
---
The Sentry profiler records several system measurements to help analyze the backtraces it gathers. Some, like CPU and heap usage, are taken on a regular sampling interval using functions from `mach/mach.h`, albeit with a lower frequency than backtrace sampling. Others, like GPU information, are taken from `CADisplayLink` callback invocations, as they're received.
diff --git a/docs/product/explore/profiling/profile-details.mdx b/docs/product/explore/profiling/profile-details.mdx
index 3f5e007d08b76..9ac4f1c239a7f 100644
--- a/docs/product/explore/profiling/profile-details.mdx
+++ b/docs/product/explore/profiling/profile-details.mdx
@@ -3,6 +3,7 @@ title: Profile Details
sidebar_order: 50
description: Learn how to explore your profile data using the Profile Details page.
og_image: /og-images/product-explore-profiling-profile-details.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/profiling/
---
Profiling can be used to gain insight into the exact functions and lines of code that are impacting performance. Each profile has a detailed view that illustrates the code that was running in your application and service while the profile was collected, with different ways to filter and visualize the data. To learn how to set up profiling and access profiling data, see the [**Profiling**](/product/explore/profiling/) documentation.
diff --git a/docs/product/explore/session-replay/mobile/index.mdx b/docs/product/explore/session-replay/mobile/index.mdx
index 91b1101d22707..44ba867d634bb 100644
--- a/docs/product/explore/session-replay/mobile/index.mdx
+++ b/docs/product/explore/session-replay/mobile/index.mdx
@@ -5,6 +5,7 @@ description: >-
Use Session Replay for Mobile to get reproductions of user sessions. You'll be
able to repro issues faster and get a better understanding of user impact.
og_image: /og-images/product-explore-session-replay-mobile.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/replays/
---
Session Replay allows you to see reproductions of user sessions, which can help you understand what happened before, during, and after an error or performance issue occurred. As you play back each session, you'll be able to see every user interaction in relation to network requests, frontend and backend errors, backend spans, and more.
diff --git a/docs/product/explore/trace-explorer/index.mdx b/docs/product/explore/trace-explorer/index.mdx
index eee36ee77f9c5..43b75c0b0e061 100644
--- a/docs/product/explore/trace-explorer/index.mdx
+++ b/docs/product/explore/trace-explorer/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to use Sentry's Trace Explorer to search span data, find distributed
traces, and debug performance issues across your entire application stack.
og_image: /og-images/product-explore-trace-explorer.png
+sentry_ui_url: https://sentry.io/organizations/{org}/explore/traces/
---
The [**Trace Explorer**](https://sentry.io/orgredirect/organizations/:orgslug/traces/) in Sentry is designed to make performance investigation easier and more intuitive. You can now explore span samples, visualize span attributes, and aggregate your data with flexible queries and filters. This guide will walk you through the key concepts and features of the Trace Explorer.
diff --git a/docs/product/issues/grouping-and-fingerprints/index.mdx b/docs/product/issues/grouping-and-fingerprints/index.mdx
index e2b0b8d19ffec..4419d905c3bd9 100644
--- a/docs/product/issues/grouping-and-fingerprints/index.mdx
+++ b/docs/product/issues/grouping-and-fingerprints/index.mdx
@@ -6,6 +6,7 @@ description: >-
Learn more about how Sentry groups issues together as well as different ways
to change how events are grouped.
og_image: /og-images/product-issues-grouping-and-fingerprints.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Have you ever had a set of similar-looking issues like this?
diff --git a/docs/product/issues/issue-details/breadcrumbs/index.mdx b/docs/product/issues/issue-details/breadcrumbs/index.mdx
index 446945a2194be..d55c29d192b8f 100644
--- a/docs/product/issues/issue-details/breadcrumbs/index.mdx
+++ b/docs/product/issues/issue-details/breadcrumbs/index.mdx
@@ -3,6 +3,7 @@ title: Using Breadcrumbs
description: Learn about using breadcrumbs on the Issue Details page to help debug faster.
sidebar_order: 60
og_image: /og-images/product-issues-issue-details-breadcrumbs.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
diff --git a/docs/product/issues/issue-details/error-issues/index.mdx b/docs/product/issues/issue-details/error-issues/index.mdx
index 8f3a21f50c3b6..14bea766b2aea 100644
--- a/docs/product/issues/issue-details/error-issues/index.mdx
+++ b/docs/product/issues/issue-details/error-issues/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to use the information on the Issue Details page to debug an error
issue.
og_image: /og-images/product-issues-issue-details-error-issues.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
An _error issue_ is a grouping of error events. What counts as an error varies by platform, but in general, if there's something that looks like an exception, it can be captured as an error in Sentry. Sentry automatically captures errors, uncaught exceptions, and unhandled rejections, as well as other types of errors, depending on platform. We group similar error events into issues based on a fingerprint. For error issues, a fingerprint is primarily defined by the event stack trace.
diff --git a/docs/product/issues/issue-details/feature-flags/index.mdx b/docs/product/issues/issue-details/feature-flags/index.mdx
index 3eb0e9d3364f6..633f64def90b9 100644
--- a/docs/product/issues/issue-details/feature-flags/index.mdx
+++ b/docs/product/issues/issue-details/feature-flags/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to set up and interact with Sentry's feature flag evaluation
tracking and feature flag change tracking.
og_image: /og-images/product-issues-issue-details-feature-flags.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
diff --git a/docs/product/issues/issue-details/index.mdx b/docs/product/issues/issue-details/index.mdx
index af13b9a15b1aa..137438d21abe0 100644
--- a/docs/product/issues/issue-details/index.mdx
+++ b/docs/product/issues/issue-details/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to navigate the Issue Details page to help you efficiently triage an
issue.
og_image: /og-images/product-issues-issue-details.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The **Issue Details** page helps you gain further insight into the source of the issue and the impact it has on your application's users.
diff --git a/docs/product/issues/issue-details/performance-issues/consecutive-db-queries/index.mdx b/docs/product/issues/issue-details/performance-issues/consecutive-db-queries/index.mdx
index c2716aa6cb8e9..f01dd640f81f3 100644
--- a/docs/product/issues/issue-details/performance-issues/consecutive-db-queries/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/consecutive-db-queries/index.mdx
@@ -4,6 +4,7 @@ sidebar_order: 30
description: Learn more about Consecutive DB Queries and how to diagnose and fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-consecutive-db-queries.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Consecutive DB Queries are a sequence of database spans where one or more have been identified as parallelizable, in other words, spans that can be shifted to the start of the sequence. This often occurs when a DB query performs no filtering on the data, for example a query without a WHERE clause.
diff --git a/docs/product/issues/issue-details/performance-issues/consecutive-http/index.mdx b/docs/product/issues/issue-details/performance-issues/consecutive-http/index.mdx
index 854ddd12c84e7..38a8cbf0d0bbe 100644
--- a/docs/product/issues/issue-details/performance-issues/consecutive-http/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/consecutive-http/index.mdx
@@ -4,6 +4,7 @@ sidebar_order: 30
description: Learn more about Consecutive HTTP issues and how to diagnose and fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-consecutive-http.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Consecutive HTTP issues are created when at least 2000ms of time can be saved by parallelizing a minimum of 3 consecutive HTTP calls occurring sequentially.
diff --git a/docs/product/issues/issue-details/performance-issues/db-main-thread-io.mdx b/docs/product/issues/issue-details/performance-issues/db-main-thread-io.mdx
index 09dd7eea22f9f..02b37ce234965 100644
--- a/docs/product/issues/issue-details/performance-issues/db-main-thread-io.mdx
+++ b/docs/product/issues/issue-details/performance-issues/db-main-thread-io.mdx
@@ -6,6 +6,7 @@ description: >-
them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-db-main-thread-io.png
+sentry_ui_url: https://sentry.io/settings/{org}/projects/docs/performance/
---
The main UI thread in a mobile application handles user interface events such as button presses and page scrolls. To prevent App Hangs and Application Not Responding errors, the main UI thread shouldn't be used for performing long-running operations like database queries. These kinds of operations block the whole UI until they finish running and get in the way of the user interacting with the app.
diff --git a/docs/product/issues/issue-details/performance-issues/endpoint-regressions/index.mdx b/docs/product/issues/issue-details/performance-issues/endpoint-regressions/index.mdx
index 9ee2e0698648b..ba954b6e41495 100644
--- a/docs/product/issues/issue-details/performance-issues/endpoint-regressions/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/endpoint-regressions/index.mdx
@@ -4,6 +4,7 @@ sidebar_order: 41
description: Learn more about Endpoint Regression issues and how to diagnose and fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-endpoint-regressions.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Endpoint Regression issues are a generic class of problems where the duration of a transaction increases over time and degrades application performance. Sentry proactively monitors common endpoints out of the box and reports any possible regressions, grouping them as Endpoint Regression issue types.
diff --git a/docs/product/issues/issue-details/performance-issues/file-main-thread-io.mdx b/docs/product/issues/issue-details/performance-issues/file-main-thread-io.mdx
index a798433c02947..d99dde15c5372 100644
--- a/docs/product/issues/issue-details/performance-issues/file-main-thread-io.mdx
+++ b/docs/product/issues/issue-details/performance-issues/file-main-thread-io.mdx
@@ -6,6 +6,7 @@ description: >-
issues.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-file-main-thread-io.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The main UI thread in a mobile application handles user interface events such as button presses and page scrolls. To prevent App Hangs and Application Not Responding errors, the main UI thread should not be used for performing long-running operations like file I/O. These kinds of operations block the whole UI until they finish running and get in the way of the user interacting with the app.
diff --git a/docs/product/issues/issue-details/performance-issues/frame-drop/index.mdx b/docs/product/issues/issue-details/performance-issues/frame-drop/index.mdx
index 42e09508355e2..4fa8322c59c55 100644
--- a/docs/product/issues/issue-details/performance-issues/frame-drop/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/frame-drop/index.mdx
@@ -5,6 +5,7 @@ description: >-
them.
sidebar_order: 30
og_image: /og-images/product-issues-issue-details-performance-issues-frame-drop.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The main (or UI) thread in a mobile app is responsible for handling all user interaction and needs to be able to respond to gestures and taps in real time. If a long-running operation blocks the main thread, the app becomes unresponsive, impacting the quality of the user experience.
diff --git a/docs/product/issues/issue-details/performance-issues/http-overhead/index.mdx b/docs/product/issues/issue-details/performance-issues/http-overhead/index.mdx
index 891e285ffe22e..3da3075cde7e8 100644
--- a/docs/product/issues/issue-details/performance-issues/http-overhead/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/http-overhead/index.mdx
@@ -3,6 +3,7 @@ title: HTTP/1.1 Overhead
sidebar_order: 30
description: Learn more about HTTP/1.1 Overhead issues and how to diagnose and fix them.
og_image: /og-images/product-issues-issue-details-performance-issues-http-overhead.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
HTTP/1.1 overhead issues are created when a set of overlapping HTTP spans for the same host are queued by the browser and have a queue time that consistently exceeds 500ms.
diff --git a/docs/product/issues/issue-details/performance-issues/image-decoding-main-thread/index.mdx b/docs/product/issues/issue-details/performance-issues/image-decoding-main-thread/index.mdx
index 7bf0f853c3f79..4e955aace0124 100644
--- a/docs/product/issues/issue-details/performance-issues/image-decoding-main-thread/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/image-decoding-main-thread/index.mdx
@@ -6,6 +6,7 @@ description: >-
fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-image-decoding-main-thread.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The main (or UI) thread in a mobile app is responsible for handling all user interaction and needs to be able to respond to gestures and taps in real time. If a long-running operation blocks the main thread, the app becomes unresponsive, impacting the quality of the user experience.
diff --git a/docs/product/issues/issue-details/performance-issues/index.mdx b/docs/product/issues/issue-details/performance-issues/index.mdx
index 875b75f0b2163..fe570899c1649 100644
--- a/docs/product/issues/issue-details/performance-issues/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn more about which performance issues Sentry detects and how to use the
Issue Details page to debug them.
og_image: /og-images/product-issues-issue-details-performance-issues.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
A _performance issue_ is a grouping of transaction events that are performing poorly. If your application is configured for tracing, that data can be used in [pre-built Sentry Dashboards](/product/dashboards/sentry-dashboards/), Sentry will detect common performance problems, and group them into issues. We group similar transaction events into issues based on a fingerprint. For performance issues, a fingerprint is primarily based on the problem type and the spans involved in the problem.
diff --git a/docs/product/issues/issue-details/performance-issues/json-decoding-main-thread/index.mdx b/docs/product/issues/issue-details/performance-issues/json-decoding-main-thread/index.mdx
index 858fe95c2b69d..0fa785d9fb75e 100644
--- a/docs/product/issues/issue-details/performance-issues/json-decoding-main-thread/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/json-decoding-main-thread/index.mdx
@@ -6,6 +6,7 @@ description: >-
fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-json-decoding-main-thread.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The main (or UI) thread in a mobile app is responsible for handling all user interaction and needs to be able to respond to gestures and taps in real time. If a long-running operation blocks the main thread, the app becomes unresponsive, impacting the quality of the user experience.
diff --git a/docs/product/issues/issue-details/performance-issues/large-http-payload/index.mdx b/docs/product/issues/issue-details/performance-issues/large-http-payload/index.mdx
index 0841076682253..82da510cd08cf 100644
--- a/docs/product/issues/issue-details/performance-issues/large-http-payload/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/large-http-payload/index.mdx
@@ -4,6 +4,7 @@ description: 'Learn more about Large HTTP Payload issues, how to diagnose and fi
sidebar_order: 30
og_image: >-
/og-images/product-issues-issue-details-performance-issues-large-http-payload.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Large HTTP Payload issues are created when an http span has an encoded body size that exceeds a threshold.
diff --git a/docs/product/issues/issue-details/performance-issues/large-render-blocking-asset/index.mdx b/docs/product/issues/issue-details/performance-issues/large-render-blocking-asset/index.mdx
index f9ff7c4a27cc3..6e812cbe5ac5c 100644
--- a/docs/product/issues/issue-details/performance-issues/large-render-blocking-asset/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/large-render-blocking-asset/index.mdx
@@ -6,6 +6,7 @@ description: >-
them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-large-render-blocking-asset.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
A _Large Render Blocking Asset_ is a performance problem that happens when a large asset causes a delay in displaying the page content. For example, if a page includes a stylesheet, and the browser pauses page rendering until the entire stylesheet downloads and processes. If that stylesheet is large, the user may see a blank or unstyled screen for a long time. This problem occurs when applications don't split up asset files, load non-essential assets synchronously, or load assets too early.
diff --git a/docs/product/issues/issue-details/performance-issues/n-one-api-calls.mdx b/docs/product/issues/issue-details/performance-issues/n-one-api-calls.mdx
index c84a9f509e479..0098e7538d966 100644
--- a/docs/product/issues/issue-details/performance-issues/n-one-api-calls.mdx
+++ b/docs/product/issues/issue-details/performance-issues/n-one-api-calls.mdx
@@ -3,6 +3,7 @@ title: N+1 API Calls
sidebar_order: 15
description: Learn more about N+1 API Calls and how to diagnose and fix them.
og_image: /og-images/product-issues-issue-details-performance-issues-n-one-api-calls.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
_N+1 API Calls_ are a performance problem that happens when an application makes many simultaneous network requests to fetch a given type of resource. For example, when an application makes one network request for each item on a list instead of fetching all the information at once in a single combined network call. Each call to the server incurs performance overhead, and if the number of calls is high, it can lead to significant performance problems. This problem commonly occurs when rendering lists or grids of items, where each item needs to fetch additional information from the server.
diff --git a/docs/product/issues/issue-details/performance-issues/n-one-queries.mdx b/docs/product/issues/issue-details/performance-issues/n-one-queries.mdx
index 1e5f9c3cd2f2e..82a0431f3f492 100644
--- a/docs/product/issues/issue-details/performance-issues/n-one-queries.mdx
+++ b/docs/product/issues/issue-details/performance-issues/n-one-queries.mdx
@@ -3,6 +3,7 @@ title: N+1 Queries
sidebar_order: 10
description: Learn more about N+1 Queries and how to diagnose and fix them.
og_image: /og-images/product-issues-issue-details-performance-issues-n-one-queries.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
_N+1 Queries_ are a performance problem in which the application makes database queries in a loop, instead of making a single query that returns or modifies all the information at once. Each database connection takes some amount of time, so querying the database in a loop can be many times slower than doing it just once. This problem often occurs when you use an object-relational mapping (ORM) tool in web frameworks like Django or Ruby on Rails.
diff --git a/docs/product/issues/issue-details/performance-issues/regex-main-thread/index.mdx b/docs/product/issues/issue-details/performance-issues/regex-main-thread/index.mdx
index 815b1583fd9f8..89e5326edb893 100644
--- a/docs/product/issues/issue-details/performance-issues/regex-main-thread/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/regex-main-thread/index.mdx
@@ -4,6 +4,7 @@ sidebar_order: 30
description: Learn more about Regex on Main Thread issues and how to diagnose and fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-regex-main-thread.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
The main (or UI) thread in a mobile app is responsible for handling all user interaction and needs to be able to respond to gestures and taps in real time. If a long-running operation blocks the main thread, the app becomes unresponsive, impacting the quality of the user experience.
diff --git a/docs/product/issues/issue-details/performance-issues/slow-db-queries/index.mdx b/docs/product/issues/issue-details/performance-issues/slow-db-queries/index.mdx
index 4b5e6a25e3aa1..c041d7e5aac71 100644
--- a/docs/product/issues/issue-details/performance-issues/slow-db-queries/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/slow-db-queries/index.mdx
@@ -3,6 +3,7 @@ title: Slow DB Queries
sidebar_order: 30
description: Learn more about Slow DB Query issues and how to diagnose and fix them.
og_image: /og-images/product-issues-issue-details-performance-issues-slow-db-queries.png
+sentry_ui_url: https://sentry.io/settings/{org}/projects/docs/performance/
---
Slow DB Query issues are created when a particular `SELECT` SQL query in your application consistently takes longer than `500ms` to resolve.
diff --git a/docs/product/issues/issue-details/performance-issues/uncompressed-asset/index.mdx b/docs/product/issues/issue-details/performance-issues/uncompressed-asset/index.mdx
index e6d9389f5f892..41cb00b317ced 100644
--- a/docs/product/issues/issue-details/performance-issues/uncompressed-asset/index.mdx
+++ b/docs/product/issues/issue-details/performance-issues/uncompressed-asset/index.mdx
@@ -4,6 +4,7 @@ sidebar_order: 30
description: Learn more about Uncompressed Asset issues and how to diagnose and fix them.
og_image: >-
/og-images/product-issues-issue-details-performance-issues-uncompressed-asset.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Uncompressed Asset issues happen when a file that needs to be downloaded in order to load a browser page, doesn’t get compressed while being transferred. This may indicate a misconfiguration of the server or CDN (content delivery network) that’s serving the asset file.
diff --git a/docs/product/issues/issue-details/replay-issues/hydration-error.mdx b/docs/product/issues/issue-details/replay-issues/hydration-error.mdx
index 2bfa537a93e31..8c7fe22b4e1c1 100644
--- a/docs/product/issues/issue-details/replay-issues/hydration-error.mdx
+++ b/docs/product/issues/issue-details/replay-issues/hydration-error.mdx
@@ -3,6 +3,7 @@ title: Hydration Error
sidebar_order: 50
description: Learn about Session Replay hydration errors.
og_image: /og-images/product-issues-issue-details-replay-issues-hydration-error.gif
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Hydration errors are a React-specific problem that happen when the initial client UI does not match what was rendered on the server. They can result in extra work for the browser, and a slower pageload experience for users.
diff --git a/docs/product/issues/issue-details/replay-issues/rage-clicks.mdx b/docs/product/issues/issue-details/replay-issues/rage-clicks.mdx
index a7c61ed5746fd..35e6c867d6a38 100644
--- a/docs/product/issues/issue-details/replay-issues/rage-clicks.mdx
+++ b/docs/product/issues/issue-details/replay-issues/rage-clicks.mdx
@@ -3,6 +3,7 @@ title: Rage Click Issues
sidebar_order: 40
description: Learn about Session Replay rage click issues.
og_image: /og-images/product-issues-issue-details-replay-issues-rage-clicks.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
If you've enabled [Session Replay](/product/explore/session-replay/), you'll be able to see rage click issues on the [**Issues**](https://sentry.io/orgredirect/organizations/:orgslug/issues/) page in Sentry. Rage clicks are a series of consecutive clicks on the same unresponsive page element. They are a strong signal of user frustration and most likely deserve your attention.
diff --git a/docs/product/issues/issue-details/uptime-issues/index.mdx b/docs/product/issues/issue-details/uptime-issues/index.mdx
index 1a321c9f05186..cb6a78195597a 100644
--- a/docs/product/issues/issue-details/uptime-issues/index.mdx
+++ b/docs/product/issues/issue-details/uptime-issues/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to use the information on the Issue Details page to debug an error
issue.
og_image: /og-images/product-issues-issue-details-uptime-issues.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
An uptime issue is a grouping of detected downtime events for a specific URL. A downtime event is generated by
diff --git a/docs/product/issues/issue-priority/index.mdx b/docs/product/issues/issue-priority/index.mdx
index 9abca8cd6b877..da7e2dae67dee 100644
--- a/docs/product/issues/issue-priority/index.mdx
+++ b/docs/product/issues/issue-priority/index.mdx
@@ -3,6 +3,7 @@ title: Issue Priority
sidebar_order: 31
description: Learn how Sentry prioritizes issue actionability.
og_image: /og-images/product-issues-issue-priority.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Issue priority sorts the issues Sentry receives into **High**, **Medium** and **Low** priority buckets. This helps you identify and address critical, high-priority errors that may be impacting your application's functionality and user experience first.
diff --git a/docs/product/issues/issue-views/index.mdx b/docs/product/issues/issue-views/index.mdx
index 6a08b1be388b8..3edd3f0301ed2 100644
--- a/docs/product/issues/issue-views/index.mdx
+++ b/docs/product/issues/issue-views/index.mdx
@@ -3,6 +3,7 @@ title: Issue Views
sidebar_order: 9
description: 'Learn how to create, customize, and share your Issues experience in Sentry.'
og_image: /og-images/product-issues-issue-views.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/views/
---
Issue views let you customize and share your issue stream, so you and your teammates can quickly see the issues that are most relevant to you.
diff --git a/docs/product/issues/ownership-rules/img/assigned_to_cog.png b/docs/product/issues/ownership-rules/img/assigned_to_cog.png
index ac5321af3d71c..676ef99b116d2 100644
Binary files a/docs/product/issues/ownership-rules/img/assigned_to_cog.png and b/docs/product/issues/ownership-rules/img/assigned_to_cog.png differ
diff --git a/docs/product/issues/ownership-rules/img/auto-assign-issue-owner.png b/docs/product/issues/ownership-rules/img/auto-assign-issue-owner.png
index 2a3a9c76c2d63..832b4db984043 100644
Binary files a/docs/product/issues/ownership-rules/img/auto-assign-issue-owner.png and b/docs/product/issues/ownership-rules/img/auto-assign-issue-owner.png differ
diff --git a/docs/product/issues/ownership-rules/img/codeowners.png b/docs/product/issues/ownership-rules/img/codeowners.png
index 1fc69ee718fc8..8c99b2d678b22 100644
Binary files a/docs/product/issues/ownership-rules/img/codeowners.png and b/docs/product/issues/ownership-rules/img/codeowners.png differ
diff --git a/docs/product/issues/ownership-rules/img/external_team_mappings.png b/docs/product/issues/ownership-rules/img/external_team_mappings.png
index 8b80cea3bcba6..6e4fcdb7a8bf0 100644
Binary files a/docs/product/issues/ownership-rules/img/external_team_mappings.png and b/docs/product/issues/ownership-rules/img/external_team_mappings.png differ
diff --git a/docs/product/issues/ownership-rules/img/external_user_mappings.png b/docs/product/issues/ownership-rules/img/external_user_mappings.png
index 26874cafdcdb4..e66a83bfee675 100644
Binary files a/docs/product/issues/ownership-rules/img/external_user_mappings.png and b/docs/product/issues/ownership-rules/img/external_user_mappings.png differ
diff --git a/docs/product/issues/ownership-rules/img/issue_details_edit_ownership_rules.png b/docs/product/issues/ownership-rules/img/issue_details_edit_ownership_rules.png
index b61cf82d8002d..c7bea03f3b719 100644
Binary files a/docs/product/issues/ownership-rules/img/issue_details_edit_ownership_rules.png and b/docs/product/issues/ownership-rules/img/issue_details_edit_ownership_rules.png differ
diff --git a/docs/product/issues/ownership-rules/img/link_stack_trace.png b/docs/product/issues/ownership-rules/img/link_stack_trace.png
index 4d5e437cfe1e8..f56eb36af24b7 100644
Binary files a/docs/product/issues/ownership-rules/img/link_stack_trace.png and b/docs/product/issues/ownership-rules/img/link_stack_trace.png differ
diff --git a/docs/product/issues/ownership-rules/img/project_settings_edit_ownership_rules.png b/docs/product/issues/ownership-rules/img/project_settings_edit_ownership_rules.png
index dd3234bacadb3..27f4ae4441a3e 100644
Binary files a/docs/product/issues/ownership-rules/img/project_settings_edit_ownership_rules.png and b/docs/product/issues/ownership-rules/img/project_settings_edit_ownership_rules.png differ
diff --git a/docs/product/issues/ownership-rules/img/suggested_assignees.png b/docs/product/issues/ownership-rules/img/suggested_assignees.png
index 3754b1a3afc03..d928209e0f4e4 100644
Binary files a/docs/product/issues/ownership-rules/img/suggested_assignees.png and b/docs/product/issues/ownership-rules/img/suggested_assignees.png differ
diff --git a/docs/product/issues/ownership-rules/index.mdx b/docs/product/issues/ownership-rules/index.mdx
index e2af27daa8d78..7742753c5580e 100644
--- a/docs/product/issues/ownership-rules/index.mdx
+++ b/docs/product/issues/ownership-rules/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to set up ownership rules to automatically assign issues to the
right owners.
og_image: /og-images/product-issues-ownership-rules.png
+sentry_ui_url: https://sentry.io/settings/{org}/projects/docs/ownership/
---
diff --git a/docs/product/issues/reprocessing/index.mdx b/docs/product/issues/reprocessing/index.mdx
index 060179a7ae040..0a8ab63a1e6a2 100644
--- a/docs/product/issues/reprocessing/index.mdx
+++ b/docs/product/issues/reprocessing/index.mdx
@@ -3,6 +3,7 @@ title: Reprocessing
sidebar_order: 50
description: Learn about reprocessing errors with new debug files.
og_image: /og-images/product-issues-reprocessing.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Sentry uses [debug information
diff --git a/docs/product/issues/states-triage/escalating-issues/index.mdx b/docs/product/issues/states-triage/escalating-issues/index.mdx
index 14715076215fd..b3d0a4a3af96a 100644
--- a/docs/product/issues/states-triage/escalating-issues/index.mdx
+++ b/docs/product/issues/states-triage/escalating-issues/index.mdx
@@ -3,6 +3,7 @@ title: Escalating Issues Algorithm
sidebar_order: 40
description: Learn how the escalating issues algorithm works.
og_image: /og-images/product-issues-states-triage-escalating-issues.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
diff --git a/docs/product/issues/states-triage/index.mdx b/docs/product/issues/states-triage/index.mdx
index 27bd69c316fbb..791aa22ca3f95 100644
--- a/docs/product/issues/states-triage/index.mdx
+++ b/docs/product/issues/states-triage/index.mdx
@@ -3,6 +3,7 @@ title: Issue Status
sidebar_order: 30
description: Learn how issue status works and and how to triage issues.
og_image: /og-images/product-issues-states-triage.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Use the status tags attached to issues on the [**Issues** page](https://sentry.io/issues/) in Sentry to help you triage and prioritize problems with your application that are important to you. Keep in mind that an issue can only have **one status at a time.**
diff --git a/docs/product/issues/suspect-commits/index.mdx b/docs/product/issues/suspect-commits/index.mdx
index 1f52065ea4915..0d0e102a0c31f 100644
--- a/docs/product/issues/suspect-commits/index.mdx
+++ b/docs/product/issues/suspect-commits/index.mdx
@@ -5,6 +5,7 @@ description: >-
With suspect commits, you can see the most recent commit to your code in the
stack trace. Learn more about how integrations enable suspect commits here.
og_image: /og-images/product-issues-suspect-commits.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
diff --git a/docs/product/monitors-and-alerts/monitors/crons/getting-started/http/index.mdx b/docs/product/monitors-and-alerts/monitors/crons/getting-started/http/index.mdx
index 8df3b86125ca8..7f692256694ff 100644
--- a/docs/product/monitors-and-alerts/monitors/crons/getting-started/http/index.mdx
+++ b/docs/product/monitors-and-alerts/monitors/crons/getting-started/http/index.mdx
@@ -2,6 +2,7 @@
title: HTTP
sidebar_order: 100
description: "Follow this guide to set up and manage monitors using the Sentry API."
+sentry_ui_url: https://sentry.io/organizations/{org}/monitors/alerts/new/
---
Sentry Crons allows you to monitor the uptime and performance of any scheduled, recurring job. Once implemented, it'll allow you to get alerts and metrics to help you solve errors, detect timeouts, and prevent disruptions to your service.
diff --git a/docs/product/onboarding/index.mdx b/docs/product/onboarding/index.mdx
index 03c9582b405bd..c6867f28de70d 100644
--- a/docs/product/onboarding/index.mdx
+++ b/docs/product/onboarding/index.mdx
@@ -3,6 +3,7 @@ title: Quick-Start Guide
description: An easy onboarding guide for Sentry.
sidebar_order: 4000
og_image: /og-images/product-onboarding.png
+sentry_ui_url: https://sentry.io/organizations/{org}/projects/new/
---
diff --git a/docs/product/relay/getting-started.mdx b/docs/product/relay/getting-started.mdx
index dfb50909562fa..21d22f2cd808b 100644
--- a/docs/product/relay/getting-started.mdx
+++ b/docs/product/relay/getting-started.mdx
@@ -3,6 +3,7 @@ title: Getting Started
description: 'Learn how to get started with Relay, Sentry''s data security solution.'
sidebar_order: 1
og_image: /og-images/product-relay-getting-started.png
+sentry_ui_url: https://sentry.io/settings/{org}/relay/
---
Getting started with [Relay](/product/relay/) is as simple as using the default settings. You can also configure Relay to suit your organization's needs. Check the [Configuration Options](../options/) page for a detailed discussion of operating scenarios.
diff --git a/docs/product/releases/health/index.mdx b/docs/product/releases/health/index.mdx
index b0ae9e1b228ec..d5778b50c6be6 100644
--- a/docs/product/releases/health/index.mdx
+++ b/docs/product/releases/health/index.mdx
@@ -5,6 +5,7 @@ description: >-
Monitor the health of releases by observing user adoption, usage of the
application, percentage of crashes, and session data.
og_image: /og-images/product-releases-health.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
diff --git a/docs/product/releases/index.mdx b/docs/product/releases/index.mdx
index a5b9f194e8ff8..9d3486f8c620f 100644
--- a/docs/product/releases/index.mdx
+++ b/docs/product/releases/index.mdx
@@ -6,6 +6,7 @@ description: >-
such as release health and release details, to determine regressions and
resolve issues faster.
og_image: /og-images/product-releases.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
diff --git a/docs/product/releases/release-details.mdx b/docs/product/releases/release-details.mdx
index 74a1ccdda9074..3451ab9b6b166 100644
--- a/docs/product/releases/release-details.mdx
+++ b/docs/product/releases/release-details.mdx
@@ -3,6 +3,7 @@ title: Release Details
sidebar_order: 40
description: Learn more about the details of individual releases.
og_image: /og-images/product-releases-release-details.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
The **Release Details** page focuses on an individual release. Elements of the release are displayed, such as visualized trends for crashes and sessions, specifics regarding each issue, adoption graphs, and commit author breakdowns.
diff --git a/docs/product/releases/releases-throughout-sentry/index.mdx b/docs/product/releases/releases-throughout-sentry/index.mdx
index e6f179242c9d7..56e1d408e878e 100644
--- a/docs/product/releases/releases-throughout-sentry/index.mdx
+++ b/docs/product/releases/releases-throughout-sentry/index.mdx
@@ -3,6 +3,7 @@ title: Using Releases Across Sentry
sidebar_order: 60
description: Learn how to use releases throughout Sentry's product.
og_image: /og-images/product-releases-releases-throughout-sentry.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
## Charts
diff --git a/docs/product/releases/setup/release-automation/circleci/index.mdx b/docs/product/releases/setup/release-automation/circleci/index.mdx
index ce52289359117..7c926c1f71c6e 100644
--- a/docs/product/releases/setup/release-automation/circleci/index.mdx
+++ b/docs/product/releases/setup/release-automation/circleci/index.mdx
@@ -4,6 +4,7 @@ description: >-
Learn how Sentry and CircleCI automate release management and help identify
suspect commits.
og_image: /og-images/product-releases-setup-release-automation-circleci.png
+sentry_ui_url: https://sentry.io/settings/{org}/integrations/
---
This guide walks you through the process of automating Sentry release management and deploy notifications in CircleCI. After deploying in CircleCI, you’ll be able to associate commits with releases. You'll also be able to apply source maps to see the original code in Sentry.
diff --git a/docs/product/releases/setup/release-automation/github-actions/index.mdx b/docs/product/releases/setup/release-automation/github-actions/index.mdx
index b7aef480bc215..8a02f7fa43548 100644
--- a/docs/product/releases/setup/release-automation/github-actions/index.mdx
+++ b/docs/product/releases/setup/release-automation/github-actions/index.mdx
@@ -4,6 +4,7 @@ description: >-
Learn how Sentry and GitHub Actions automate release management, upload source
maps and help identify suspect commits.
og_image: /og-images/product-releases-setup-release-automation-github-actions.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
The [Sentry Release GitHub Action](https://github.com/marketplace/actions/sentry-release) automates Sentry release management in GitHub with just one step. After sending Sentry release information, you’ll be able to associate commits with releases. Additionally, you can upload source maps to view original, un-minified source code.
diff --git a/docs/product/releases/setup/release-automation/index.mdx b/docs/product/releases/setup/release-automation/index.mdx
index 5dc80621283cb..a610d50291850 100644
--- a/docs/product/releases/setup/release-automation/index.mdx
+++ b/docs/product/releases/setup/release-automation/index.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how Sentry release management can be automated using any continuous
integration and delivery (CI/CD) platform or automation server.
og_image: /og-images/product-releases-setup-release-automation.png
+sentry_ui_url: https://sentry.io/settings/{org}/integrations/
---
Sentry release management can be automated using any continuous integration and delivery (CI/CD) platform or automation server.
diff --git a/docs/product/releases/setup/release-automation/jenkins/index.mdx b/docs/product/releases/setup/release-automation/jenkins/index.mdx
index 7399828923267..784b3aa84b1c7 100644
--- a/docs/product/releases/setup/release-automation/jenkins/index.mdx
+++ b/docs/product/releases/setup/release-automation/jenkins/index.mdx
@@ -4,6 +4,7 @@ description: >-
Learn how Sentry and Jenkins automate release management and help identify
suspect commits.
og_image: /og-images/product-releases-setup-release-automation-jenkins.png
+sentry_ui_url: https://sentry.io/settings/{org}/integrations/
---
This guide walks you through the process of automating Sentry release management and deploy notifications in Jenkins. After deploying in Jenkins, you’ll be able to associate commits with releases. You'll also be able to apply source maps to see the original code in Sentry.
diff --git a/docs/product/releases/setup/release-automation/travis-ci/index.mdx b/docs/product/releases/setup/release-automation/travis-ci/index.mdx
index e0626be70542b..89c0007c6c526 100644
--- a/docs/product/releases/setup/release-automation/travis-ci/index.mdx
+++ b/docs/product/releases/setup/release-automation/travis-ci/index.mdx
@@ -4,6 +4,7 @@ description: >-
Learn how Sentry and Travis CI automate release management and help identify
suspect commits.
og_image: /og-images/product-releases-setup-release-automation-travis-ci.png
+sentry_ui_url: https://sentry.io/settings/{org}/integrations/
---
This guide walks you through the process of automating Sentry release management and deploy notifications in Travis CI. After deploying in Travis CI, you’ll be able to identify suspect commits that are likely the culprit for new errors. You’ll also be able to apply source maps to see the original code in Sentry.
diff --git a/docs/product/releases/usage/archive-release.mdx b/docs/product/releases/usage/archive-release.mdx
index a05e8d694ec5c..1e88afe4b486b 100644
--- a/docs/product/releases/usage/archive-release.mdx
+++ b/docs/product/releases/usage/archive-release.mdx
@@ -3,6 +3,7 @@ title: Archive a Release
sidebar_order: 30
description: Learn how to permanently hide or archive a release.
og_image: /og-images/product-releases-usage-archive-release.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
Since an event received from a deleted release automatically recreates it, Sentry has an option to hide, or archive, releases. Archiving can also be useful if you're no longer interested in viewing a release or if you've accidentally created a release you no longer want to track.
diff --git a/docs/product/releases/usage/restore-release.mdx b/docs/product/releases/usage/restore-release.mdx
index f8e8c3b4ca21b..577f18fcd3743 100644
--- a/docs/product/releases/usage/restore-release.mdx
+++ b/docs/product/releases/usage/restore-release.mdx
@@ -3,6 +3,7 @@ title: Restore a Release
sidebar_order: 40
description: Learn how to restore an archived release.
og_image: /og-images/product-releases-usage-restore-release.png
+sentry_ui_url: https://sentry.io/organizations/{org}/releases/
---
You can restore archived releases using either [sentry.io](https://sentry.io) or the Sentry CLI.
diff --git a/docs/product/sentry-basics/distributed-tracing/initialize-sentry-sdk-frontend.mdx b/docs/product/sentry-basics/distributed-tracing/initialize-sentry-sdk-frontend.mdx
index 0a8cd85c66df9..01dade41dfa40 100644
--- a/docs/product/sentry-basics/distributed-tracing/initialize-sentry-sdk-frontend.mdx
+++ b/docs/product/sentry-basics/distributed-tracing/initialize-sentry-sdk-frontend.mdx
@@ -4,6 +4,7 @@ sidebar_order: 3
description: Learn how to add the Sentry SDK to your frontend codebase.
og_image: >-
/og-images/product-sentry-basics-distributed-tracing-initialize-sentry-sdk-frontend.png
+sentry_ui_url: none
---
This section walks you through how to import the sample application into your local development environment, add the Sentry SDK, and initialize it.
diff --git a/docs/product/sentry-basics/integrate-backend/capturing-errors.mdx b/docs/product/sentry-basics/integrate-backend/capturing-errors.mdx
index 40d46edda95c5..33d7d252887ed 100644
--- a/docs/product/sentry-basics/integrate-backend/capturing-errors.mdx
+++ b/docs/product/sentry-basics/integrate-backend/capturing-errors.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how Sentry captures unhandled errors and handled errors to enrich your
event data.
og_image: /og-images/product-sentry-basics-integrate-backend-capturing-errors.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Once initialized in your code, the Sentry SDK will capture errors and various types of events to notify you about them in real-time, depending on the alert rules you've configured. With the Django app already running on your [localhost](http://localhost:8000/), let's try them out.
diff --git a/docs/product/sentry-basics/integrate-backend/configuration-options.mdx b/docs/product/sentry-basics/integrate-backend/configuration-options.mdx
index bda342daf0f1d..d9cb13dbee043 100644
--- a/docs/product/sentry-basics/integrate-backend/configuration-options.mdx
+++ b/docs/product/sentry-basics/integrate-backend/configuration-options.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to configure releases, breadcrumbs, and environments to enhance the
SDK's functionality.
og_image: /og-images/product-sentry-basics-integrate-backend-configuration-options.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Sentry has various configuration options to help enhance the SDK functionality. The options can help provide additional data needed to debug issues even faster or to help control what's sent to Sentry by filtering. Learn more in [Configuration](/platforms/python/configuration/).
diff --git a/docs/product/sentry-basics/integrate-backend/getting-started.mdx b/docs/product/sentry-basics/integrate-backend/getting-started.mdx
index 017dedbee3755..8cf38f66c0995 100644
--- a/docs/product/sentry-basics/integrate-backend/getting-started.mdx
+++ b/docs/product/sentry-basics/integrate-backend/getting-started.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to add and initialize the Sentry SDK for a sample backend
application.
og_image: /og-images/product-sentry-basics-integrate-backend-getting-started.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
In this tutorial, you'll import the backend app source code into your local development environment, add the Sentry SDK, and initialize it.
diff --git a/docs/product/sentry-basics/integrate-frontend/configure-scms.mdx b/docs/product/sentry-basics/integrate-frontend/configure-scms.mdx
index 994bff6ed778c..4eeeba72a3f93 100644
--- a/docs/product/sentry-basics/integrate-frontend/configure-scms.mdx
+++ b/docs/product/sentry-basics/integrate-frontend/configure-scms.mdx
@@ -6,6 +6,7 @@ description: >-
commits and surface suspect commits and suggested assignees to help you
resolve issues faster.
og_image: /og-images/product-sentry-basics-integrate-frontend-configure-scms.png
+sentry_ui_url: https://sentry.io/settings/{org}/integrations/github/328965/?tab=codeMappings
---
diff --git a/docs/product/sentry-basics/integrate-frontend/generate-first-error.mdx b/docs/product/sentry-basics/integrate-frontend/generate-first-error.mdx
index 3370bce69490d..604ac8a6ee524 100644
--- a/docs/product/sentry-basics/integrate-frontend/generate-first-error.mdx
+++ b/docs/product/sentry-basics/integrate-frontend/generate-first-error.mdx
@@ -3,6 +3,7 @@ title: Capture Your First Error
sidebar_order: 3
description: Learn how to capture your first error and view it in Sentry.
og_image: /og-images/product-sentry-basics-integrate-frontend-generate-first-error.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
Now that the sample app is up and running on your local environment integrated with the Sentry SDK, you're ready to generate the first error.
diff --git a/docs/product/sentry-basics/integrate-frontend/initialize-sentry-sdk.mdx b/docs/product/sentry-basics/integrate-frontend/initialize-sentry-sdk.mdx
index 51ffb4d11a552..4a837fca9e4ed 100644
--- a/docs/product/sentry-basics/integrate-frontend/initialize-sentry-sdk.mdx
+++ b/docs/product/sentry-basics/integrate-frontend/initialize-sentry-sdk.mdx
@@ -3,6 +3,7 @@ title: Add the Sentry SDK to Your Project
sidebar_order: 2
description: Learn how to add the Sentry SDK to your frontend codebase.
og_image: /og-images/product-sentry-basics-integrate-frontend-initialize-sentry-sdk.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
This section walks you through how to import the sample application into your local development environment, add the Sentry SDK, and initialize it.
diff --git a/docs/product/sentry-basics/integrate-frontend/upload-source-maps.mdx b/docs/product/sentry-basics/integrate-frontend/upload-source-maps.mdx
index 0d586f15747f9..a68708a1bb016 100644
--- a/docs/product/sentry-basics/integrate-frontend/upload-source-maps.mdx
+++ b/docs/product/sentry-basics/integrate-frontend/upload-source-maps.mdx
@@ -5,6 +5,7 @@ description: >-
Learn how to upload source maps to enable readable stack traces in your
errors.
og_image: /og-images/product-sentry-basics-integrate-frontend-upload-source-maps.png
+sentry_ui_url: https://sentry.io/organizations/{org}/issues/
---
When creating web applications, most development workflows involve transforming your JavaScript code via transpilation and minification to make it run more efficiently in the browser. Source map files serve as a guide for tools like Sentry to convert the transformed version of your JavaScript back to the code that you wrote. Source map files can be generated by your JavaScript build tool.
diff --git a/docs/product/sentry-toolbar/setup.mdx b/docs/product/sentry-toolbar/setup.mdx
index 2b4e58e126970..0fc411162edbd 100644
--- a/docs/product/sentry-toolbar/setup.mdx
+++ b/docs/product/sentry-toolbar/setup.mdx
@@ -5,6 +5,7 @@ description: >-
Get started with Sentry's Toolbar, bringing critical Sentry insights and tools
into your web app to help your team troubleshoot more effectively.
og_image: /og-images/product-sentry-toolbar-setup.png
+sentry_ui_url: https://sentry.io/settings/{org}/sentry-apps/toolbar/
---
diff --git a/docs/product/user-feedback/index.mdx b/docs/product/user-feedback/index.mdx
index 15124fabe473f..c862d6fafb86a 100644
--- a/docs/product/user-feedback/index.mdx
+++ b/docs/product/user-feedback/index.mdx
@@ -3,6 +3,7 @@ title: User Feedback
sidebar_order: 65
description: Learn how you can view and triage user feedback submissions.
og_image: /og-images/product-user-feedback.png
+sentry_ui_url: https://sentry.io/organizations/{org}/feedback/
---
Sentry automatically detects errors thrown by your application, such as performance issues and user experience problems like rage clicks. But there are other frustrations your users may encounter (broken permission flows, broken links, typos, misleading UX, business logic flaws, and so on).
diff --git a/scripts/screenshot-pipeline/PRD.md b/scripts/screenshot-pipeline/PRD.md
new file mode 100644
index 0000000000000..ee2625d4527fc
--- /dev/null
+++ b/scripts/screenshot-pipeline/PRD.md
@@ -0,0 +1,105 @@
+# PRD: Automated Documentation Screenshot Pipeline
+
+**Team:** Sentry Documentation
+**Status:** Draft
+**Last updated:** 2026-03-24
+
+---
+
+## 1. Problem Statement
+
+The Sentry docs contain thousands of pages with stale screenshots and Arcade embeds left over from a major UI refresh. Manually auditing and replacing these is prohibitively time-consuming. The docs team needs an automated pipeline that replaces screenshots where confidence is high, creates Linear issues for everything requiring human judgment, and keeps screenshots current on an ongoing basis.
+
+## 2. Goals
+
+1. **Eliminate stale screenshots** — automatically detect and replace screenshots of deterministic UI pages where the diff confidence is high.
+2. **Triage the rest** — automatically create Linear issues for low-confidence diffs, annotated images, Arcade embeds, and auth-complex pages so nothing falls through the cracks.
+3. **Prevent future staleness** — run the pipeline on a schedule (and on deploy triggers) so screenshots stay current without manual effort.
+4. **Empower writers** — provide a single local command that lets any writer regenerate a screenshot without engineering help.
+
+## 3. Non-Goals
+
+- Re-recording Arcade interactive walkthroughs (detection is in scope; re-recording is manual).
+- Auto-regenerating annotated screenshots (callouts, arrows, highlights). These will be ticketed.
+- Building a visual review dashboard (e.g. Percy). This is deferred unless review workflow demands it.
+
+## 4. Success Metrics
+
+| Metric | Target |
+|--------|--------|
+| Stale screenshots auto-replaced (POC set) | >= 70% of deterministic, non-annotated screenshots |
+| Linear issues created for remaining items | 100% of items that don't meet auto-replace criteria |
+| Time from UI deploy to screenshot update (steady state) | < 24 hours |
+| Manual effort per screenshot refresh cycle | Near zero for auto-replaced; ticket-driven for the rest |
+
+## 5. User Stories
+
+- **As a docs writer**, I want stale screenshots replaced automatically so I don't have to manually re-capture UI screens after every product update.
+- **As a docs writer**, I want a Linear issue created for any screenshot that needs human judgment so I have a clear worklist.
+- **As a docs writer**, I want to run a single command locally to regenerate any screenshot so I'm not blocked on engineering.
+- **As a docs team lead**, I want the pipeline to run on a schedule so screenshots never go stale again.
+- **As a docs team lead**, I want visibility into what was auto-replaced vs. ticketed so I can plan review and manual work.
+
+## 6. Classification Logic
+
+Each stale asset is classified into one of five buckets:
+
+| Scenario | Action |
+|----------|--------|
+| Deterministic UI page, clean diff | Auto-replace screenshot, commit |
+| Significant diff but page has dynamic content | Linear issue -- needs human review |
+| Screenshot has annotations, arrows, or callouts | Linear issue -- needs manual redo |
+| Arcade embed | Linear issue -- needs re-recording |
+| Auth-complex or state-dependent page | Linear issue -- needs scripting work first |
+
+## 7. Known Limitations & Risks
+
+- **Annotated screenshots** — Playwright captures raw UI, not overlaid annotations. These must be redone manually.
+- **Arcade embeds** — No automation can re-record interactive walkthroughs. Detection is automatic; re-recording is not.
+- **Confidence scoring** — Pixel diff tells you something changed, not whether the new screenshot is correct. Plan for a lightweight human review pass on auto-replaced images before publishing.
+- **Auth flows** — Session management in capture scripts requires real engineering effort. Budget accordingly.
+
+## 8. Rollout Plan
+
+### Phase 1: Proof of Concept
+
+- **Week 1:** Playwright script crawls 20-30 representative doc pages, captures fresh screenshots, diffs against existing images. Validate auth flows and capture quality.
+- **Week 2:** Wire up Linear issue creation for non-auto-replaceable items. Connect capture scripts to a GitHub Actions workflow.
+
+### Phase 2: Full Pipeline (post-POC)
+
+- Expand crawl to full docs inventory.
+- Tune diff thresholds and classification heuristics.
+- Enable scheduled and deploy-triggered runs.
+- Add local CLI command for writers.
+- Conduct human review pass on first full auto-replace batch.
+
+### Phase 3: Steady State
+
+- Pipeline runs on schedule and on deploy triggers.
+- New screenshots are captured automatically; edge cases are ticketed.
+- Optional: evaluate Percy or similar visual review dashboard if review workflow warrants it.
+
+## 9. Dependencies
+
+- Access to a Sentry staging/demo environment with representative data for captures.
+- Sentry auth credentials (service account or token) for Playwright session management.
+- Linear API access (via MCP or API token) for automated issue creation.
+- GitHub Actions runner with Playwright browser dependencies.
+
+---
+
+## Decisions & Clarifications (POC)
+
+| Item | Decision |
+|------|----------|
+| UI_REFRESH_CUTOFF | June 2025 (configurable) |
+| Staging Environment | Use personal credentials for POC |
+| Linear Team | DOCS team, label `Playwright` |
+| POC Pages | `/product/insights/` section |
+| Image Directories | `docs/*/img/` and `includes/*/images/` |
+| Annotated Screenshots | Rare; manual override in config |
+| Arcade Focus | Pre-June 2025 embeds prioritized |
+| PR Reviewers | `getsentry/docs`, `getsentry/product-owners-docs` |
+| Local CLI Auth | Deferred to post-POC |
+| Image Optimization | Yes - optimize before commit |
diff --git a/scripts/screenshot-pipeline/README.md b/scripts/screenshot-pipeline/README.md
new file mode 100644
index 0000000000000..a02c1101f814c
--- /dev/null
+++ b/scripts/screenshot-pipeline/README.md
@@ -0,0 +1,166 @@
+# Screenshot Pipeline
+
+Automated screenshot capture, diff, and replacement pipeline for Sentry documentation. Detects stale screenshots, auto-replaces high-confidence matches, and creates Linear issues for everything requiring manual review.
+
+## How It Works
+
+1. **Crawl** -- Scans MDX files for image references and Arcade embeds, checks Git history for staleness
+2. **Capture** -- Uses Playwright to visit Sentry UI pages and take fresh screenshots
+3. **Diff** -- Compares old vs new screenshots using pixel-level diffing
+4. **Replace or Ticket** -- Auto-replaces high-confidence diffs in a PR; creates Linear issues for the rest
+
+## Quick Start
+
+```bash
+cd scripts/screenshot-pipeline
+npm install
+
+# First time: authenticate with Sentry (opens a browser)
+npm run auth:setup
+
+# Run the full pipeline
+npm run crawl # discover stale assets
+npm run capture # screenshot + diff
+npm run replace # commit replacements + open PR
+npm run create-issues # create Linear issues for manual items
+
+# Or run everything in sequence
+npm run pipeline
+```
+
+## For Docs Writers
+
+### `sentry_ui_url` frontmatter field
+
+Docs pages with screenshots of the Sentry UI need a `sentry_ui_url` field in their frontmatter:
+
+```yaml
+---
+title: Your Page Title
+description: Page description.
+sentry_ui_url: https://sentry.io/organizations/{org}/your/page/path/
+---
+```
+
+Use `{org}` as a placeholder -- the pipeline replaces it with the actual org slug at capture time.
+
+**This field is added automatically.** When you open a PR that adds images to a docs page, a GitHub Actions workflow detects the missing field, infers the URL from the doc path, and commits it to your branch. You don't need to do anything.
+
+If the auto-detected URL is wrong (e.g., your screenshots show a specific sub-page rather than the section landing page), just update the field manually -- the bot won't overwrite an existing value.
+
+Pages that don't need `sentry_ui_url`:
+- Pages with only code samples, architecture diagrams, or non-UI images
+- Pages with only Arcade embeds (handled separately)
+- Tutorial pages where screenshots show IDEs, terminals, or external tools
+
+### Checking your screenshots locally
+
+```bash
+# Capture a specific image
+npm run capture -- --asset docs/product/issues/img/issue-details.png
+
+# Capture all images for a doc page
+npm run capture -- --doc docs/product/issues/issue-details/index.mdx
+
+# Preview without overwriting anything
+npm run capture -- --dry-run
+```
+
+## Commands Reference
+
+| Command | Description |
+|---------|-------------|
+| `npm run auth:setup` | Open a browser to log in to Sentry and save the session |
+| `npm run crawl` | Scan docs for stale screenshots and Arcade embeds |
+| `npm run capture` | Capture fresh screenshots and compute diffs |
+| `npm run replace` | Copy auto-replaceable screenshots and open a PR |
+| `npm run create-issues` | Create Linear issues for items needing manual review |
+| `npm run auto-map` | Auto-generate URL mappings from doc paths (for bulk setup) |
+| `npm run inject-urls` | Inject `sentry_ui_url` into MDX frontmatter from auto-map output |
+| `npm run generate-map` | Generate a template for `screenshot-map.yaml` |
+| `npm run pipeline` | Run the full pipeline (crawl + capture + replace + create-issues) |
+
+### CLI Flags
+
+Most commands accept these flags:
+
+- `--scope docs/product/issues` -- Limit to a specific directory
+- `--dry-run` -- Preview changes without modifying files
+- `--asset ` -- Target a specific image (capture only)
+- `--doc ` -- Target all images in a specific doc (capture only)
+
+## Configuration
+
+### Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `UI_REFRESH_CUTOFF` | `2025-06-01` | Images last modified before this date are considered stale |
+| `DIFF_THRESHOLD_LOW` | `0.01` (1%) | Below this diff % = unchanged, skip |
+| `DIFF_THRESHOLD_HIGH` | `0.50` (50%) | Above this diff % = suspicious, flag for review |
+| `SENTRY_ORG_SLUG` | `sentry` | Org slug for URL templating |
+| `SENTRY_STORAGE_STATE_PATH` | `auth/storageState.json` | Path to Playwright auth state |
+| `LINEAR_API_KEY` | -- | Linear API token (required for issue creation) |
+| `LINEAR_TEAM_ID` | -- | Linear team ID for the DOCS team |
+| `DRY_RUN` | `false` | Set to `true` to prevent any mutations |
+
+### Config Files
+
+- **`config/screenshot-map.yaml`** -- Override URL mappings and add element selectors, ignore regions, etc. Takes precedence over frontmatter for individual images.
+- **`config/screenshot-config.yaml`** -- Flag images as annotated or skipped.
+
+## Diff Thresholds
+
+The pipeline uses three thresholds to classify screenshot diffs:
+
+| Diff % | Deterministic Page | Result |
+|--------|-------------------|--------|
+| < 1% | either | `unchanged` -- skip |
+| 1-50% | `true` | `auto_replace` -- goes into PR |
+| 1-50% | `false` | `needs_review` -- Linear issue |
+| > 50% | either | `needs_review` -- suspiciously large change |
+| capture fails | either | `capture_failed` -- Linear issue |
+
+Since we use a demo account with controlled data, most pages are marked `deterministic: true` and eligible for auto-replacement.
+
+## Auth
+
+The pipeline authenticates to Sentry using Playwright's storage state mechanism (saved cookies/session).
+
+```bash
+# Generate auth state (opens a browser, log in, then close it)
+npm run auth:setup
+
+# For CI: base64-encode and store as a GitHub secret
+base64 -i auth/storageState.json | pbcopy
+```
+
+The auth state file is gitignored. When it expires, re-run `npm run auth:setup`.
+
+## Architecture
+
+See [TECH-SPEC.md](TECH-SPEC.md) for full technical details and [PRD.md](PRD.md) for product requirements.
+
+```
+scripts/screenshot-pipeline/
+ src/
+ crawl-inventory.ts # Discovers stale assets
+ capture-and-diff.ts # Playwright capture + pixelmatch diff
+ auto-replace.ts # Commits replacements + opens PR
+ create-linear-issues.ts # Creates Linear tickets
+ lib/
+ types.ts # Shared types + config loader
+ auth.ts # Sentry auth helper
+ diff.ts # pixelmatch wrapper
+ image-optimizer.ts # sharp-based PNG optimization
+ linear-client.ts # Linear API client
+ tools/
+ auto-map-urls.ts # Bulk URL mapping from doc paths
+ inject-frontmatter-urls.ts # Inject sentry_ui_url into MDX frontmatter
+ generate-map-template.ts # Generate screenshot-map.yaml template
+ config/
+ screenshot-map.yaml # Per-image overrides
+ screenshot-config.yaml # Annotated image flags
+ auth/
+ storageState.json # Playwright session (gitignored)
+```
diff --git a/scripts/screenshot-pipeline/TECH-SPEC.md b/scripts/screenshot-pipeline/TECH-SPEC.md
new file mode 100644
index 0000000000000..2f9e466d3d060
--- /dev/null
+++ b/scripts/screenshot-pipeline/TECH-SPEC.md
@@ -0,0 +1,293 @@
+# Tech Spec: Automated Documentation Screenshot Pipeline
+
+**Status:** Draft
+**Last updated:** 2026-03-24
+
+---
+
+## 1. Architecture Overview
+
+The system consists of four components that run sequentially in a GitHub Actions workflow:
+
+```
+[1. Inventory Crawler] -> [2. Screenshot Capturer + Differ] -> [3. Auto-Replacer] -> [4. Linear Issue Creator]
+```
+
+All components are Playwright scripts (TypeScript/Node.js) orchestrated by GitHub Actions. No external SaaS dependencies are required.
+
+## 2. Component Details
+
+### 2.1 Inventory Crawler
+
+**Purpose:** Scan the docs repo, identify every screenshot and Arcade embed, and classify staleness.
+
+**Input:** Docs repo file tree + Git history.
+
+**Process:**
+
+1. Walk the docs content directory (`docs/` and `includes/`). Parse each Markdown/MDX file.
+2. Extract all image references (``, `
` tags) and Arcade embed references (`` components or iframe embeds).
+3. For each asset, query Git history (`git log --follow -1 --format=%aI -- `) to get the last-modified date.
+4. Compare last-modified date against a configurable `UI_REFRESH_CUTOFF` date. Flag assets modified before the cutoff as stale.
+5. Classify each stale asset:
+ - `arcade_embed` -- detected by component name or iframe src pattern.
+ - `annotated_screenshot` -- detected by manual override in `screenshot-config.yaml`.
+ - `standard_screenshot` -- everything else.
+
+**Output:** A JSON manifest file:
+
+```json
+[
+ {
+ "doc_path": "docs/product/alerts/index.mdx",
+ "asset_path": "docs/product/alerts/img/alert-listing.png",
+ "asset_type": "standard_screenshot",
+ "last_modified": "2025-01-15T10:30:00Z",
+ "is_stale": true,
+ "ui_page_url": null,
+ "element_selector": null
+ }
+]
+```
+
+**Annotation Detection:** Annotated screenshots are rare in this codebase and there are no naming conventions. Detection relies on:
+
+1. Manual override file (`screenshot-config.yaml`) where writers can flag images as annotated.
+2. Future: color histogram check for annotation-typical overlays (bright red/yellow/orange).
+
+### 2.2 Screenshot Capturer + Differ
+
+**Purpose:** For each stale `standard_screenshot`, navigate to the corresponding Sentry UI page, capture a fresh screenshot, and compute a pixel diff.
+
+**Input:** The inventory manifest (filtered to `standard_screenshot` entries that have a `ui_page_url` populated via `screenshot-map.yaml`).
+
+**Prerequisites:** Each screenshot must be mapped to a Sentry UI URL and an optional CSS selector. This mapping lives in a config file (`screenshot-map.yaml`):
+
+```yaml
+- asset_path: docs/product/alerts/img/alert-listing.png
+ ui_page_url: https://sentry.io/organizations/{org}/alerts/rules/
+ element_selector: null # null = full viewport
+ viewport: { width: 1280, height: 800 }
+ auth_required: true
+ deterministic: true
+ wait_for: null # optional CSS selector to wait for
+ ignore_regions: [] # only for truly ephemeral content (timestamps, user avatars)
+ # NOTE: DO include nav/sidebar/top bar -- they changed in the UI refresh
+ # and detecting those changes is the whole point.
+```
+
+**Process:**
+
+1. Launch Playwright Chromium browser.
+2. Authenticate to Sentry (see section 3 Auth Strategy).
+3. For each entry:
+ a. Navigate to `ui_page_url`. Wait for network idle + configurable selector visibility.
+ b. If `element_selector` is set, capture element screenshot. Otherwise, capture full viewport.
+ c. Save capture as PNG to a temp directory.
+ d. Optimize image using `sharp` (compress while maintaining quality).
+ e. Run pixel diff (using `pixelmatch` library) between the existing image and the new capture.
+ f. Compute diff percentage: `changed_pixels / total_pixels`.
+ g. Record result:
+ - `diff_pct`: the percentage of changed pixels
+ - `capture_path`: path to the new screenshot
+ - `status`: classification (see below)
+
+**Diff Classification (Three-Threshold Model):**
+
+| Condition | Status |
+|-----------|--------|
+| `diff_pct < DIFF_THRESHOLD_LOW` (default: 1%) | `unchanged` -- skip, no action needed |
+| `diff_pct >= DIFF_THRESHOLD_LOW` AND `< DIFF_THRESHOLD_HIGH` AND `deterministic: true` | `auto_replace` |
+| `diff_pct >= DIFF_THRESHOLD_LOW` AND `< DIFF_THRESHOLD_HIGH` AND `deterministic: false` | `needs_review` |
+| `diff_pct >= DIFF_THRESHOLD_HIGH` (default: 50%) | `needs_review` -- suspiciously large change |
+| Capture failed (timeout, selector not found, auth error) | `capture_failed` |
+
+**Output:** Enriched manifest with diff results.
+
+**Configurable thresholds (env vars):**
+
+- `DIFF_THRESHOLD_LOW`: minimum diff % to consider a screenshot changed (default: `0.01` / 1%)
+- `DIFF_THRESHOLD_HIGH`: maximum diff % before flagging for review even on deterministic pages (default: `0.50` / 50%)
+- `CAPTURE_TIMEOUT_MS`: navigation timeout per page (default: `30000`)
+- `WAIT_FOR_SELECTOR_TIMEOUT_MS`: element wait timeout (default: `10000`)
+
+### 2.3 Auto-Replacer
+
+**Purpose:** For all `auto_replace` entries, copy the new capture over the old image and commit.
+
+**Process:**
+
+1. Filter manifest to `status: auto_replace`.
+2. For each entry, copy the captured (and optimized) PNG to the original `asset_path`, overwriting the old image.
+3. Stage all changed files. Create a single Git commit: `chore(docs): auto-replace N stale screenshots`.
+4. Push to a new branch: `docs/auto-screenshot-update-{date}`.
+5. Open a pull request with a summary table of all replacements for human review before merge.
+ - Reviewers: `getsentry/docs`, `getsentry/product-owners-docs`
+
+### 2.4 Linear Issue Creator
+
+**Purpose:** Create one Linear issue per item that requires human intervention.
+
+**Input:** Manifest entries with status `needs_review`, `capture_failed`, `annotated_screenshot`, or `arcade_embed`.
+
+**Process:**
+
+1. Connect to Linear via `@linear/sdk`.
+2. For each entry, create an issue:
+ - **Title:** `[Docs Screenshot] Update {asset_path}` or `[Docs Arcade] Re-record {asset_path}`
+ - **Description:** Include doc page URL, current screenshot (as image link), classification reason, and diff percentage (if applicable).
+ - **Labels:** `docs-screenshots`, `Playwright`, plus classification label (`needs-review`, `annotated`, `arcade`, `auth-complex`).
+ - **Team:** DOCS (configurable via env var `LINEAR_TEAM_ID`).
+3. Deduplicate: before creating, check if an open issue with the same `asset_path` in the title already exists. Skip if so.
+
+**Output:** List of created Linear issue IDs, logged to the workflow summary.
+
+## 3. Auth Strategy
+
+Playwright authenticates to Sentry using a service account. Implementation:
+
+1. **Initial setup:** Run `npx playwright codegen` to interactively log in and capture the auth flow. Save the resulting storage state (`storageState.json`) as a GitHub Actions secret (base64-encoded).
+2. **In CI:** Decode the secret, write to disk, and pass as `storageState` in `browser.newContext()`.
+3. **Token refresh:** If using API token auth instead of session cookies, store the token as a GitHub secret and inject via env var. Playwright scripts pass it as a cookie or header.
+4. **Fallback:** If auth state expires mid-run, the capture script catches the auth redirect, logs the failure as `capture_failed`, and continues.
+
+## 4. Linear Integration
+
+Use `@linear/sdk` npm package with a `LINEAR_API_KEY` stored as a GitHub secret.
+
+The script abstracts this behind an interface so the integration method can be swapped without changing the pipeline logic.
+
+## 5. GitHub Actions Workflow
+
+```yaml
+name: Screenshot Pipeline
+
+on:
+ schedule:
+ - cron: '0 6 * * 1' # Weekly on Monday at 6am UTC
+ workflow_dispatch: # Manual trigger
+
+jobs:
+ screenshot-pipeline:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history for git log queries
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - uses: pnpm/action-setup@v2
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps chromium
+
+ - name: Restore auth state
+ run: echo "$SENTRY_STORAGE_STATE" | base64 -d > /tmp/storageState.json
+ env:
+ SENTRY_STORAGE_STATE: ${{ secrets.SENTRY_STORAGE_STATE }}
+
+ - name: Run inventory crawler
+ run: npx ts-node scripts/screenshot-pipeline/src/crawl-inventory.ts
+ env:
+ UI_REFRESH_CUTOFF: '2025-06-01'
+
+ - name: Run screenshot capture & diff
+ run: npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts
+ env:
+ SENTRY_STORAGE_STATE_PATH: /tmp/storageState.json
+ SENTRY_ORG_SLUG: ${{ secrets.SENTRY_ORG_SLUG }}
+
+ - name: Auto-replace and open PR
+ run: npx ts-node scripts/screenshot-pipeline/src/auto-replace.ts
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create Linear issues
+ run: npx ts-node scripts/screenshot-pipeline/src/create-linear-issues.ts
+ env:
+ LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
+ LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID }}
+```
+
+## 6. File Structure
+
+```
+scripts/screenshot-pipeline/
+ ├── PRD.md # Product requirements
+ ├── TECH-SPEC.md # This document
+ ├── package.json # Pipeline-specific dependencies
+ ├── tsconfig.json # TypeScript config
+ ├── playwright.config.ts # Playwright configuration
+ ├── src/
+ │ ├── crawl-inventory.ts # Inventory Crawler
+ │ ├── capture-and-diff.ts # Screenshot Capturer + Differ
+ │ ├── auto-replace.ts # Auto-Replacer (commit + PR)
+ │ ├── create-linear-issues.ts # Linear Issue Creator
+ │ └── lib/
+ │ ├── types.ts # Shared TypeScript types
+ │ ├── auth.ts # Sentry auth helper
+ │ ├── diff.ts # pixelmatch wrapper
+ │ ├── linear-client.ts # Linear API abstraction
+ │ └── image-optimizer.ts # Image optimization (sharp)
+ ├── config/
+ │ ├── screenshot-map.yaml # URL + selector mapping per screenshot
+ │ └── screenshot-config.yaml # Manual overrides (annotated flags, etc.)
+ ├── tools/
+ │ └── generate-map-template.ts # Helper to generate screenshot-map template
+ └── output/ # Pipeline output (gitignored)
+ ├── inventory-manifest.json
+ ├── diff-results.json
+ └── captures/ # Temporary screenshot captures
+```
+
+## 7. Dependencies
+
+| Package | Purpose |
+|---------|---------|
+| `playwright` | Browser automation and screenshot capture |
+| `pixelmatch` | Pixel-level image diffing |
+| `pngjs` | PNG read/write for pixelmatch |
+| `sharp` | Image optimization/compression |
+| `js-yaml` | Parse YAML config files |
+| `@linear/sdk` | Linear API client |
+| `@octokit/rest` | GitHub PR creation |
+| `gray-matter` | Parse MDX frontmatter |
+| `glob` | File tree walking |
+| `commander` | CLI argument parsing |
+
+## 8. Local Developer Usage (Post-POC)
+
+Writers can regenerate any screenshot locally:
+
+```shell
+# Regenerate a single screenshot
+npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --asset docs/product/alerts/img/alert-listing.png
+
+# Regenerate all screenshots for a doc page
+npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --doc docs/product/alerts/index.mdx
+
+# Dry run -- capture but don't overwrite
+npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --dry-run
+```
+
+## 9. POC Scope
+
+### Step 1 deliverables:
+
+- `crawl-inventory.ts` working against 20-30 representative doc pages.
+- `capture-and-diff.ts` working with auth against Sentry staging.
+- `screenshot-map.yaml` populated for the POC page set.
+- Validated: auth flows work, captures are usable, diff thresholds are sane.
+
+### Step 2 deliverables:
+
+- `create-linear-issues.ts` creating real issues in a test Linear project.
+- `auto-replace.ts` opening a PR with replaced screenshots.
+- GitHub Actions workflow running end-to-end on manual trigger.
+- Decision doc: observed auto-replace accuracy, recommended thresholds, go/no-go for full rollout.
diff --git a/scripts/screenshot-pipeline/auth/.gitignore b/scripts/screenshot-pipeline/auth/.gitignore
new file mode 100644
index 0000000000000..5fb49fc17ceb2
--- /dev/null
+++ b/scripts/screenshot-pipeline/auth/.gitignore
@@ -0,0 +1,2 @@
+# Auth state contains session cookies -- never commit this
+storageState.json
diff --git a/scripts/screenshot-pipeline/auth/README.md b/scripts/screenshot-pipeline/auth/README.md
new file mode 100644
index 0000000000000..92adca1240ea1
--- /dev/null
+++ b/scripts/screenshot-pipeline/auth/README.md
@@ -0,0 +1,31 @@
+# Auth Setup
+
+This directory holds the Playwright session state for authenticating against Sentry.
+
+## First-time setup
+
+Run:
+
+```bash
+npm run auth:setup
+```
+
+This opens a Chromium browser. Log in to Sentry as you normally would, then **close the browser window**. Your session is saved to `auth/storageState.json`.
+
+## Refreshing
+
+When captures start failing with `capture_failed` (auth redirect), your session has expired. Just re-run:
+
+```bash
+npm run auth:setup
+```
+
+## CI usage
+
+For GitHub Actions, base64-encode the storage state and store it as a secret:
+
+```bash
+base64 -i auth/storageState.json | pbcopy # copies to clipboard on macOS
+```
+
+Then add it as the `SENTRY_STORAGE_STATE` repository secret.
diff --git a/scripts/screenshot-pipeline/config/screenshot-config.yaml b/scripts/screenshot-pipeline/config/screenshot-config.yaml
new file mode 100644
index 0000000000000..fc62361e6fab6
--- /dev/null
+++ b/scripts/screenshot-pipeline/config/screenshot-config.yaml
@@ -0,0 +1,18 @@
+# Screenshot Configuration - Manual Overrides
+#
+# Use this file to flag images that need special handling.
+# The pipeline checks this before processing each image.
+#
+# Fields:
+# asset_path: Path to the image file relative to repo root
+# annotated: true if the image has callouts, arrows, or highlights
+# skip: true to skip this image entirely
+# notes: Optional explanation
+
+# Example:
+# - asset_path: docs/product/alerts/img/alert-details-annotated.png
+# annotated: true
+# skip: false
+# notes: "Has red callout boxes pointing to alert conditions"
+
+# --- Add entries below ---
diff --git a/scripts/screenshot-pipeline/config/screenshot-map.yaml b/scripts/screenshot-pipeline/config/screenshot-map.yaml
new file mode 100644
index 0000000000000..d4d1179e9ee32
--- /dev/null
+++ b/scripts/screenshot-pipeline/config/screenshot-map.yaml
@@ -0,0 +1,34 @@
+# Screenshot URL Mapping
+#
+# Maps each screenshot to the Sentry UI page it depicts.
+# The capture script uses this to navigate and screenshot the correct page.
+#
+# Fields:
+# asset_path: Path to the image file relative to repo root
+# ui_page_url: Full Sentry URL. Use {org} as placeholder for the org slug.
+# element_selector: CSS selector for element screenshot; null for full viewport.
+# viewport: { width, height } for the browser viewport.
+# auth_required: Whether the page needs Sentry authentication.
+# deterministic: Whether the page looks the same every time (no dynamic data).
+# wait_for: Optional CSS selector to wait for before capturing.
+# ignore_regions: Optional list of {x, y, width, height} regions to mask from diff.
+#
+# Tips:
+# - Start with full viewport (element_selector: null) and add selectors as needed.
+# - Mark pages with user-specific data as deterministic: false.
+# - DO include nav, sidebar, and top bar in captures -- these changed in the UI
+# refresh and are a valid signal of staleness.
+# - Only use ignore_regions for truly ephemeral content that changes every render:
+# timestamps, notification badge counts, "last seen X ago" strings, user avatars.
+
+# --- POC: /product/insights/ section ---
+
+# Example entry (uncomment and fill in real values):
+# - asset_path: docs/product/insights/img/insights-overview.png
+# ui_page_url: https://sentry.io/organizations/{org}/insights/
+# element_selector: null
+# viewport: { width: 1280, height: 800 }
+# auth_required: true
+# deterministic: true
+# wait_for: null
+# ignore_regions: [] # keep empty unless masking timestamps or user-specific data
diff --git a/scripts/screenshot-pipeline/package-lock.json b/scripts/screenshot-pipeline/package-lock.json
new file mode 100644
index 0000000000000..a30aa4fa9b46c
--- /dev/null
+++ b/scripts/screenshot-pipeline/package-lock.json
@@ -0,0 +1,1738 @@
+{
+ "name": "@sentry-docs/screenshot-pipeline",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@sentry-docs/screenshot-pipeline",
+ "version": "0.1.0",
+ "dependencies": {
+ "@linear/sdk": "^28.0.0",
+ "@octokit/rest": "^21.0.0",
+ "commander": "^12.0.0",
+ "glob": "^10.3.0",
+ "gray-matter": "^4.0.3",
+ "js-yaml": "^4.1.0",
+ "pixelmatch": "^5.3.0",
+ "playwright": "^1.48.0",
+ "pngjs": "^7.0.0",
+ "sharp": "^0.33.0"
+ },
+ "devDependencies": {
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^20.0.0",
+ "@types/pixelmatch": "^5.2.6",
+ "@types/pngjs": "^6.0.4",
+ "ts-node": "^10.9.0",
+ "typescript": "^5.4.0"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@graphql-typed-document-node/core": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@linear/sdk": {
+ "version": "28.0.0",
+ "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-28.0.0.tgz",
+ "integrity": "sha512-s8KWjI9YOFcTXFM/HpBjD80ezuQRy/pBrINK5GWWAl8gfRvJk0YbQM5KGAs2Wz48OsWigCgn4FHI4EEPiHmEeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-typed-document-node/core": "^3.1.0",
+ "graphql": "^15.4.0",
+ "isomorphic-unfetch": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12.x",
+ "yarn": "1.x"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
+ "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz",
+ "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^5.0.0",
+ "@octokit/graphql": "^8.2.2",
+ "@octokit/request": "^9.2.3",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "before-after-hook": "^3.0.2",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "10.1.4",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
+ "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
+ "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^9.2.3",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
+ "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "11.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
+ "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^13.10.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
+ "version": "24.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
+ "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
+ "version": "13.10.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
+ "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^24.2.0"
+ }
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz",
+ "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "13.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz",
+ "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^13.10.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
+ "version": "24.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
+ "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
+ "version": "13.10.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
+ "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^24.2.0"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "9.2.4",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz",
+ "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^10.1.4",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^2.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "6.1.8",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
+ "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/rest": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
+ "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/core": "^6.1.4",
+ "@octokit/plugin-paginate-rest": "^11.4.2",
+ "@octokit/plugin-request-log": "^5.3.1",
+ "@octokit/plugin-rest-endpoint-methods": "^13.3.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
+ "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.1.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.41",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
+ "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pixelmatch": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.6.tgz",
+ "integrity": "sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/pngjs": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
+ "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/before-after-hook": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-content-type-parse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
+ "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/graphql": {
+ "version": "15.10.2",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.2.tgz",
+ "integrity": "sha512-1PRqdDPAmViWr4h1GVBT8RoPZfWSGZa7kDzleTilOfVIslsgf+cia3Nl95v1KDmR4iERPaT7WzQ+tN4MJmbg3w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/isomorphic-unfetch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+ "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.6.1",
+ "unfetch": "^4.2.0"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pixelmatch": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
+ "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "pngjs": "^6.0.0"
+ },
+ "bin": {
+ "pixelmatch": "bin/pixelmatch"
+ }
+ },
+ "node_modules/pixelmatch/node_modules/pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.60.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unfetch": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==",
+ "license": "MIT"
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+ "license": "ISC"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/scripts/screenshot-pipeline/package.json b/scripts/screenshot-pipeline/package.json
new file mode 100644
index 0000000000000..e60a1061d313f
--- /dev/null
+++ b/scripts/screenshot-pipeline/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@sentry-docs/screenshot-pipeline",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Automated screenshot capture, diff, and replacement pipeline for Sentry documentation",
+ "scripts": {
+ "auth:setup": "npx playwright install chromium && npx playwright codegen --save-storage=auth/storageState.json https://sentry.io",
+ "crawl": "ts-node src/crawl-inventory.ts",
+ "capture": "ts-node src/capture-and-diff.ts",
+ "replace": "ts-node src/auto-replace.ts",
+ "create-issues": "ts-node src/create-linear-issues.ts",
+ "generate-map": "ts-node tools/generate-map-template.ts",
+ "auto-map": "ts-node tools/auto-map-urls.ts",
+ "inject-urls": "ts-node tools/inject-frontmatter-urls.ts",
+ "lint:screenshots": "ts-node tools/lint-frontmatter-urls.ts",
+ "pipeline": "npm run crawl && npm run capture && npm run replace && npm run create-issues"
+ },
+ "dependencies": {
+ "@linear/sdk": "^28.0.0",
+ "@octokit/rest": "^21.0.0",
+ "commander": "^12.0.0",
+ "glob": "^10.3.0",
+ "gray-matter": "^4.0.3",
+ "js-yaml": "^4.1.0",
+ "pixelmatch": "^5.3.0",
+ "playwright": "^1.48.0",
+ "pngjs": "^7.0.0",
+ "sharp": "^0.33.0"
+ },
+ "devDependencies": {
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^20.0.0",
+ "@types/pixelmatch": "^5.2.6",
+ "@types/pngjs": "^6.0.4",
+ "ts-node": "^10.9.0",
+ "typescript": "^5.4.0"
+ }
+}
diff --git a/scripts/screenshot-pipeline/playwright.config.ts b/scripts/screenshot-pipeline/playwright.config.ts
new file mode 100644
index 0000000000000..e631dbb4f496e
--- /dev/null
+++ b/scripts/screenshot-pipeline/playwright.config.ts
@@ -0,0 +1,23 @@
+import {defineConfig} from 'playwright/test';
+
+export default defineConfig({
+ timeout: 60_000,
+ use: {
+ // Default browser settings for screenshot capture
+ browserName: 'chromium',
+ viewport: {width: 1280, height: 800},
+ // Reduce motion and animations for consistent captures
+ reducedMotion: 'reduce',
+ // Default navigation timeout
+ navigationTimeout: 30_000,
+ // Consistent locale and timezone
+ locale: 'en-US',
+ timezoneId: 'America/Los_Angeles',
+ // Disable GPU for consistent rendering in CI
+ launchOptions: {
+ args: ['--disable-gpu', '--disable-software-rasterizer'],
+ },
+ },
+ // No test runner -- we use Playwright as a library
+ // This config is imported by capture scripts for consistent defaults
+});
diff --git a/scripts/screenshot-pipeline/src/auto-replace.ts b/scripts/screenshot-pipeline/src/auto-replace.ts
new file mode 100644
index 0000000000000..5dc50540c7844
--- /dev/null
+++ b/scripts/screenshot-pipeline/src/auto-replace.ts
@@ -0,0 +1,225 @@
+#!/usr/bin/env ts-node
+
+/**
+ * Auto-Replacer (Section 2.3 of Tech Spec)
+ *
+ * For all auto_replace entries in the diff results, copies the new capture
+ * over the old image, commits the changes, and opens a PR.
+ *
+ * Usage:
+ * npx ts-node scripts/screenshot-pipeline/src/auto-replace.ts
+ * npx ts-node scripts/screenshot-pipeline/src/auto-replace.ts --dry-run
+ */
+
+import {execSync} from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import {Octokit} from '@octokit/rest';
+import {DiffResult, loadPipelineConfig} from './lib/types';
+
+// ── Main ──
+
+async function main() {
+ const config = loadPipelineConfig();
+ const repoRoot = findRepoRoot();
+ const dryRun = process.argv.includes('--dry-run') || config.dry_run;
+
+ console.log('=== Screenshot Pipeline: Auto-Replacer ===');
+ console.log(`Dry run: ${dryRun}`);
+ console.log('');
+
+ // Load diff results
+ const resultsPath = path.join(repoRoot, config.output_dir, 'diff-results.json');
+ if (!fs.existsSync(resultsPath)) {
+ console.error(`Error: Diff results not found at ${resultsPath}`);
+ console.error('Run the capture + diff step first: npm run capture');
+ process.exit(1);
+ }
+
+ const results: DiffResult[] = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
+
+ // Filter to auto-replaceable items
+ const toReplace = results.filter(
+ r => r.status === 'auto_replace' && r.capture_path && fs.existsSync(r.capture_path)
+ );
+
+ console.log(`Found ${toReplace.length} screenshots to auto-replace\n`);
+
+ if (toReplace.length === 0) {
+ console.log('No screenshots to auto-replace.');
+ return;
+ }
+
+ // Copy new captures over old images
+ const replaced: {assetPath: string; diffPct: number}[] = [];
+
+ for (const result of toReplace) {
+ const targetPath = path.join(repoRoot, result.inventory_item.asset_path);
+ const sourcePath = result.capture_path!;
+
+ console.log(`Replacing: ${result.inventory_item.asset_path}`);
+ console.log(` Diff: ${((result.diff_pct || 0) * 100).toFixed(2)}%`);
+
+ if (dryRun) {
+ console.log(` [DRY RUN] Would copy ${sourcePath} -> ${targetPath}`);
+ } else {
+ fs.mkdirSync(path.dirname(targetPath), {recursive: true});
+ fs.copyFileSync(sourcePath, targetPath);
+ }
+
+ replaced.push({
+ assetPath: result.inventory_item.asset_path,
+ diffPct: result.diff_pct || 0,
+ });
+ }
+
+ if (dryRun) {
+ console.log(`\n[DRY RUN] Would commit and create PR for ${replaced.length} replacements.`);
+ return;
+ }
+
+ // Git operations
+ const dateStr = new Date().toISOString().slice(0, 10);
+ const branchName = `docs/auto-screenshot-update-${dateStr}`;
+ const commitMsg = `chore(docs): auto-replace ${replaced.length} stale screenshots`;
+
+ console.log(`\nCreating branch: ${branchName}`);
+
+ try {
+ // Configure git identity for CI
+ execSync('git config user.name "github-actions[bot]"', {cwd: repoRoot, stdio: 'pipe'});
+ execSync('git config user.email "github-actions[bot]@users.noreply.github.com"', {cwd: repoRoot, stdio: 'pipe'});
+
+ // Create and switch to new branch
+ execSync(`git checkout -b ${branchName}`, {cwd: repoRoot, stdio: 'pipe'});
+
+ // Stage all replaced images
+ for (const r of replaced) {
+ execSync(`git add "${r.assetPath}"`, {cwd: repoRoot, stdio: 'pipe'});
+ }
+
+ // Commit
+ execSync(`git commit -m "${commitMsg}"`, {cwd: repoRoot, stdio: 'pipe'});
+
+ console.log(`Committed: ${commitMsg}`);
+
+ // Push
+ execSync(`git push -u origin ${branchName}`, {cwd: repoRoot, stdio: 'pipe'});
+ console.log(`Pushed to: ${branchName}`);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ console.error(`Git operations failed: ${errorMsg}`);
+ console.error('You may need to manually commit and push the changes.');
+ return;
+ }
+
+ // Create PR
+ const githubToken = process.env.GITHUB_TOKEN;
+ if (!githubToken) {
+ console.warn('Warning: GITHUB_TOKEN not set. Skipping PR creation.');
+ console.log('Push succeeded. Create a PR manually.');
+ return;
+ }
+
+ await createPullRequest(githubToken, branchName, replaced, commitMsg);
+}
+
+// ── PR Creation ──
+
+async function createPullRequest(
+ token: string,
+ branchName: string,
+ replaced: {assetPath: string; diffPct: number}[],
+ title: string
+): Promise {
+ const octokit = new Octokit({auth: token});
+
+ // Build PR body
+ const tableRows = replaced
+ .map(
+ r =>
+ `| \`${r.assetPath}\` | ${(r.diffPct * 100).toFixed(2)}% |`
+ )
+ .join('\n');
+
+ const body = `## Automated Screenshot Replacement
+
+This PR was generated by the screenshot pipeline. It replaces **${replaced.length}** stale screenshots with fresh captures from the Sentry UI.
+
+### Replaced Screenshots
+
+| Asset Path | Diff % |
+|------------|--------|
+${tableRows}
+
+### Review Checklist
+
+- [ ] Spot-check a few screenshots to verify they look correct
+- [ ] Verify no annotated screenshots were accidentally replaced
+- [ ] Check that alt text is still appropriate for the new captures
+
+### Notes
+
+- All screenshots were captured from the Sentry staging environment
+- Images were optimized before committing
+- Only screenshots marked as \`deterministic: true\` with diff below the high threshold were auto-replaced
+- Screenshots with diff above ${50}% or marked \`deterministic: false\` were sent to Linear for manual review
+`;
+
+ try {
+ const {data: pr} = await octokit.pulls.create({
+ owner: 'getsentry',
+ repo: 'sentry-docs',
+ title,
+ body,
+ head: branchName,
+ base: 'master',
+ });
+
+ console.log(`\nPR created: ${pr.html_url}`);
+
+ // Request reviewers
+ try {
+ await octokit.pulls.requestReviewers({
+ owner: 'getsentry',
+ repo: 'sentry-docs',
+ pull_number: pr.number,
+ team_reviewers: ['docs', 'product-owners-docs'],
+ });
+ console.log('Reviewers requested: getsentry/docs, getsentry/product-owners-docs');
+ } catch (err) {
+ console.warn(`Warning: Could not request reviewers: ${err}`);
+ }
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ console.error(`Failed to create PR: ${errorMsg}`);
+ }
+}
+
+// ── Helpers ──
+
+function findRepoRoot(): string {
+ let dir = __dirname;
+ for (let i = 0; i < 10; i++) {
+ const pkgPath = path.join(dir, 'package.json');
+ if (fs.existsSync(pkgPath)) {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
+ if (pkg.name === 'sentry-docs') {
+ return dir;
+ }
+ } catch {
+ // continue
+ }
+ }
+ dir = path.dirname(dir);
+ }
+ return process.cwd();
+}
+
+// ── Entry Point ──
+
+main().catch(err => {
+ console.error('Auto-replace failed:', err);
+ process.exit(1);
+});
diff --git a/scripts/screenshot-pipeline/src/capture-and-diff.ts b/scripts/screenshot-pipeline/src/capture-and-diff.ts
new file mode 100644
index 0000000000000..8dab13649b8f0
--- /dev/null
+++ b/scripts/screenshot-pipeline/src/capture-and-diff.ts
@@ -0,0 +1,456 @@
+#!/usr/bin/env ts-node
+
+/**
+ * Screenshot Capturer + Differ (Section 2.2 of Tech Spec)
+ *
+ * For each stale standard_screenshot with a URL mapping, navigates to the
+ * Sentry UI page, captures a fresh screenshot, computes a pixel diff, and
+ * classifies the result.
+ *
+ * Usage:
+ * npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts
+ * npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --asset docs/product/insights/img/foo.png
+ * npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --doc docs/product/insights/index.mdx
+ * npx ts-node scripts/screenshot-pipeline/src/capture-and-diff.ts --dry-run
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as yaml from 'js-yaml';
+import {
+ AssetInventoryItem,
+ DiffResult,
+ DiffStatus,
+ ScreenshotMapEntry,
+ loadPipelineConfig,
+} from './lib/types';
+import {
+ createAuthenticatedSession,
+ closeSession,
+ isAuthRedirect,
+ resolveOrgUrl,
+ AuthenticatedSession,
+} from './lib/auth';
+import {computeDiff} from './lib/diff';
+import {optimizeImage} from './lib/image-optimizer';
+
+// ── Main ──
+
+async function main() {
+ const config = loadPipelineConfig();
+ const repoRoot = findRepoRoot();
+
+ // Parse CLI args
+ const assetFilter = getCliArg('--asset');
+ const docFilter = getCliArg('--doc');
+ const dryRun = process.argv.includes('--dry-run') || config.dry_run;
+
+ console.log('=== Screenshot Pipeline: Capturer + Differ ===');
+ console.log(`Repo root: ${repoRoot}`);
+ console.log(`Dry run: ${dryRun}`);
+ console.log(`Diff threshold low: ${(config.diff_threshold_low * 100).toFixed(1)}%`);
+ console.log(`Diff threshold high: ${(config.diff_threshold_high * 100).toFixed(1)}%`);
+ console.log('');
+
+ // Load inventory manifest
+ const manifestPath = path.join(repoRoot, config.output_dir, 'inventory-manifest.json');
+ if (!fs.existsSync(manifestPath)) {
+ console.error(`Error: Inventory manifest not found at ${manifestPath}`);
+ console.error('Run the inventory crawler first: npm run crawl');
+ process.exit(1);
+ }
+
+ const inventory: AssetInventoryItem[] = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
+
+ // Load screenshot map for per-image overrides (element selectors, ignore regions, etc.)
+ const screenshotMap = loadScreenshotMap(repoRoot);
+
+ // Filter to capturable items: stale standard screenshots with a URL
+ // URL comes from either: frontmatter (in manifest) or screenshot-map.yaml
+ let targets = inventory.filter(item => {
+ if (item.asset_type !== 'standard_screenshot') return false;
+ if (!item.is_stale) return false;
+
+ // Has a URL from frontmatter or screenshot-map.yaml
+ const mapEntry = screenshotMap.get(item.asset_path);
+ const hasUrl = item.ui_page_url || mapEntry?.ui_page_url;
+ if (!hasUrl) return false;
+
+ return true;
+ });
+
+ // Apply CLI filters
+ if (assetFilter) {
+ targets = targets.filter(t => t.asset_path === assetFilter || t.asset_path.includes(assetFilter));
+ }
+ if (docFilter) {
+ targets = targets.filter(t => t.doc_path === docFilter || t.doc_path.includes(docFilter));
+ }
+
+ // Build effective map entries: frontmatter URL + screenshot-map.yaml overrides
+ const enrichedTargets = targets.map(item => {
+ const mapOverride = screenshotMap.get(item.asset_path);
+ const effectiveEntry: ScreenshotMapEntry = {
+ asset_path: item.asset_path,
+ ui_page_url: mapOverride?.ui_page_url || item.ui_page_url || '',
+ element_selector: mapOverride?.element_selector || item.element_selector || null,
+ viewport: mapOverride?.viewport || {width: 1280, height: 800},
+ auth_required: mapOverride?.auth_required ?? true,
+ deterministic: mapOverride?.deterministic ?? true,
+ wait_for: mapOverride?.wait_for || null,
+ ignore_regions: mapOverride?.ignore_regions || [],
+ };
+ return {item, mapEntry: effectiveEntry};
+ });
+
+ console.log(`Found ${enrichedTargets.length} screenshots to capture\n`);
+
+ if (enrichedTargets.length === 0) {
+ console.log('No screenshots to capture. Ensure:');
+ console.log(' 1. Inventory manifest exists (run crawl first)');
+ console.log(' 2. MDX files have sentry_ui_url in frontmatter, or screenshot-map.yaml has entries');
+ console.log(' 3. Stale screenshots exist that match the mapping');
+ writeEmptyResults(repoRoot, config.output_dir);
+ return;
+ }
+
+ // Create output directories
+ const capturesDir = path.join(repoRoot, config.output_dir, 'captures');
+ const diffsDir = path.join(repoRoot, config.output_dir, 'diffs');
+ fs.mkdirSync(capturesDir, {recursive: true});
+ fs.mkdirSync(diffsDir, {recursive: true});
+
+ // Resolve storage state path relative to repo root
+ if (config.storage_state_path && !path.isAbsolute(config.storage_state_path)) {
+ config.storage_state_path = path.join(repoRoot, config.storage_state_path);
+ }
+
+ // Launch browser
+ let session: AuthenticatedSession | null = null;
+ if (!dryRun) {
+ console.log('Launching browser...');
+ session = await createAuthenticatedSession(config);
+ console.log('Browser launched\n');
+ }
+
+ const results: DiffResult[] = [];
+
+ try {
+ for (let i = 0; i < enrichedTargets.length; i++) {
+ const {item, mapEntry} = enrichedTargets[i];
+ const progress = `[${i + 1}/${enrichedTargets.length}]`;
+
+ console.log(`${progress} Processing: ${item.asset_path}`);
+
+ if (dryRun) {
+ console.log(` [DRY RUN] Would capture: ${mapEntry.ui_page_url}`);
+ results.push({
+ inventory_item: item,
+ capture_path: null,
+ diff_pct: null,
+ diff_image_path: null,
+ status: 'unchanged',
+ error: null,
+ captured_at: new Date().toISOString(),
+ });
+ continue;
+ }
+
+ const result = await captureAndDiff(
+ item,
+ mapEntry,
+ repoRoot,
+ capturesDir,
+ diffsDir,
+ session!,
+ config
+ );
+
+ results.push(result);
+
+ console.log(` Status: ${result.status}`);
+ if (result.diff_pct !== null) {
+ console.log(` Diff: ${(result.diff_pct * 100).toFixed(2)}%`);
+ }
+ if (result.error) {
+ console.log(` Error: ${result.error}`);
+ }
+ console.log('');
+ }
+ } finally {
+ if (session) {
+ await closeSession(session);
+ console.log('Browser closed');
+ }
+ }
+
+ // Write results
+ const resultsPath = path.join(repoRoot, config.output_dir, 'diff-results.json');
+ fs.writeFileSync(resultsPath, JSON.stringify(results, null, 2));
+
+ // Print summary
+ printSummary(results);
+ console.log(`\nResults written to: ${resultsPath}`);
+}
+
+// ── Capture Logic ──
+
+async function captureAndDiff(
+ item: AssetInventoryItem,
+ mapEntry: ScreenshotMapEntry,
+ repoRoot: string,
+ capturesDir: string,
+ diffsDir: string,
+ session: AuthenticatedSession,
+ config: ReturnType
+): Promise {
+ const capturedAt = new Date().toISOString();
+
+ // Generate file paths
+ const safeFileName = item.asset_path.replace(/[/\\]/g, '__');
+ const capturePath = path.join(capturesDir, safeFileName);
+ const diffImagePath = path.join(diffsDir, `diff__${safeFileName}`);
+
+ try {
+ // Resolve the URL
+ const url = resolveOrgUrl(mapEntry.ui_page_url, config.sentry_org_slug);
+
+ // Create a new page
+ const page = await session.context.newPage();
+
+ try {
+ // Navigate -- use 'load' instead of 'networkidle' since Sentry's SPA
+ // makes ongoing API calls that prevent networkidle from resolving
+ console.log(` Navigating to: ${url}`);
+ await page.goto(url, {
+ waitUntil: 'load',
+ timeout: config.capture_timeout_ms,
+ });
+
+ // Check for auth redirect
+ if (isAuthRedirect(page.url())) {
+ return {
+ inventory_item: item,
+ capture_path: null,
+ diff_pct: null,
+ diff_image_path: null,
+ status: 'capture_failed',
+ error: `Auth redirect detected: landed on ${page.url()}`,
+ captured_at: capturedAt,
+ };
+ }
+
+ // Wait for optional selector
+ if (mapEntry.wait_for) {
+ try {
+ await page.waitForSelector(mapEntry.wait_for, {
+ timeout: config.wait_for_selector_timeout_ms,
+ });
+ } catch {
+ console.warn(` Warning: wait_for selector "${mapEntry.wait_for}" timed out`);
+ }
+ }
+
+ // Wait for the page content to settle (SPA rendering + API responses)
+ await page.waitForTimeout(5000);
+
+ // Set viewport if specified
+ if (mapEntry.viewport) {
+ await page.setViewportSize(mapEntry.viewport);
+ // Brief wait after viewport change for re-render
+ await page.waitForTimeout(500);
+ }
+
+ // Capture screenshot
+ if (mapEntry.element_selector) {
+ // Element screenshot
+ const element = await page.$(mapEntry.element_selector);
+ if (!element) {
+ return {
+ inventory_item: item,
+ capture_path: null,
+ diff_pct: null,
+ diff_image_path: null,
+ status: 'capture_failed',
+ error: `Element not found: ${mapEntry.element_selector}`,
+ captured_at: capturedAt,
+ };
+ }
+ await element.screenshot({path: capturePath, type: 'png'});
+ } else {
+ // Full viewport screenshot
+ await page.screenshot({path: capturePath, type: 'png', fullPage: false});
+ }
+
+ console.log(` Captured: ${capturePath}`);
+
+ // Optimize the captured image
+ const optimizeResult = await optimizeImage(capturePath, capturePath);
+ if (optimizeResult.ratio < 1) {
+ const savedPct = ((1 - optimizeResult.ratio) * 100).toFixed(1);
+ console.log(` Optimized: ${savedPct}% size reduction`);
+ }
+ } finally {
+ await page.close();
+ }
+
+ // Compute diff against existing image
+ const existingImagePath = path.join(repoRoot, item.asset_path);
+ if (!fs.existsSync(existingImagePath)) {
+ return {
+ inventory_item: item,
+ capture_path: capturePath,
+ diff_pct: 1.0,
+ diff_image_path: null,
+ status: classifyDiff(1.0, mapEntry.deterministic, config),
+ error: `Existing image not found: ${existingImagePath}`,
+ captured_at: capturedAt,
+ };
+ }
+
+ const diffOutput = await computeDiff(existingImagePath, capturePath, diffImagePath, {
+ ignoreRegions: mapEntry.ignore_regions || [],
+ generateDiffImage: true,
+ });
+
+ const status = classifyDiff(diffOutput.diffPct, mapEntry.deterministic, config);
+
+ return {
+ inventory_item: item,
+ capture_path: capturePath,
+ diff_pct: diffOutput.diffPct,
+ diff_image_path: diffOutput.diffImagePath,
+ status,
+ error: null,
+ captured_at: capturedAt,
+ };
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ return {
+ inventory_item: item,
+ capture_path: null,
+ diff_pct: null,
+ diff_image_path: null,
+ status: 'capture_failed',
+ error: errorMsg,
+ captured_at: capturedAt,
+ };
+ }
+}
+
+// ── Classification ──
+
+function classifyDiff(
+ diffPct: number,
+ deterministic: boolean,
+ config: ReturnType
+): DiffStatus {
+ if (diffPct < config.diff_threshold_low) {
+ return 'unchanged';
+ }
+
+ if (diffPct >= config.diff_threshold_high) {
+ // Suspiciously large change -- even deterministic pages get flagged
+ return 'needs_review';
+ }
+
+ if (deterministic) {
+ return 'auto_replace';
+ }
+
+ return 'needs_review';
+}
+
+// ── Helpers ──
+
+function loadScreenshotMap(repoRoot: string): Map {
+ const mapPath = path.join(
+ repoRoot,
+ 'scripts/screenshot-pipeline/config/screenshot-map.yaml'
+ );
+ const map = new Map();
+
+ if (!fs.existsSync(mapPath)) {
+ return map;
+ }
+
+ try {
+ const raw = fs.readFileSync(mapPath, 'utf-8');
+ const entries = yaml.load(raw) as ScreenshotMapEntry[] | null;
+ if (Array.isArray(entries)) {
+ for (const entry of entries) {
+ map.set(entry.asset_path, entry);
+ }
+ }
+ } catch (err) {
+ console.warn(`Warning: Could not parse screenshot-map.yaml: ${err}`);
+ }
+
+ return map;
+}
+
+function findRepoRoot(): string {
+ let dir = __dirname;
+ for (let i = 0; i < 10; i++) {
+ const pkgPath = path.join(dir, 'package.json');
+ if (fs.existsSync(pkgPath)) {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
+ if (pkg.name === 'sentry-docs') {
+ return dir;
+ }
+ } catch {
+ // continue
+ }
+ }
+ dir = path.dirname(dir);
+ }
+ return process.cwd();
+}
+
+function getCliArg(flag: string): string | null {
+ const idx = process.argv.indexOf(flag);
+ if (idx >= 0 && idx + 1 < process.argv.length) {
+ return process.argv[idx + 1];
+ }
+ return null;
+}
+
+function writeEmptyResults(repoRoot: string, outputDir: string): void {
+ const resultsPath = path.join(repoRoot, outputDir, 'diff-results.json');
+ fs.mkdirSync(path.dirname(resultsPath), {recursive: true});
+ fs.writeFileSync(resultsPath, JSON.stringify([], null, 2));
+}
+
+function printSummary(results: DiffResult[]): void {
+ const byStatus: Record = {};
+ for (const r of results) {
+ byStatus[r.status] = (byStatus[r.status] || 0) + 1;
+ }
+
+ console.log('\n=== Capture + Diff Summary ===');
+ console.log(`Total processed: ${results.length}`);
+ for (const [status, count] of Object.entries(byStatus)) {
+ console.log(` ${status}: ${count}`);
+ }
+
+ // Show diff distribution
+ const withDiffs = results.filter(r => r.diff_pct !== null);
+ if (withDiffs.length > 0) {
+ const diffs = withDiffs.map(r => r.diff_pct!);
+ const avg = diffs.reduce((a, b) => a + b, 0) / diffs.length;
+ const min = Math.min(...diffs);
+ const max = Math.max(...diffs);
+ console.log(`\nDiff distribution (${withDiffs.length} images):`);
+ console.log(` Min: ${(min * 100).toFixed(2)}%`);
+ console.log(` Max: ${(max * 100).toFixed(2)}%`);
+ console.log(` Avg: ${(avg * 100).toFixed(2)}%`);
+ }
+}
+
+// ── Entry Point ──
+
+main().catch(err => {
+ console.error('Capture + Diff failed:', err);
+ process.exit(1);
+});
diff --git a/scripts/screenshot-pipeline/src/crawl-inventory.ts b/scripts/screenshot-pipeline/src/crawl-inventory.ts
new file mode 100644
index 0000000000000..fc9a33aa3ddbc
--- /dev/null
+++ b/scripts/screenshot-pipeline/src/crawl-inventory.ts
@@ -0,0 +1,542 @@
+#!/usr/bin/env ts-node
+
+/**
+ * Inventory Crawler (Section 2.1 of Tech Spec)
+ *
+ * Scans the docs repo, identifies every screenshot and Arcade embed,
+ * classifies staleness based on Git history, and outputs a JSON manifest.
+ *
+ * Usage:
+ * npx ts-node scripts/screenshot-pipeline/src/crawl-inventory.ts
+ * npx ts-node scripts/screenshot-pipeline/src/crawl-inventory.ts --scope docs/product/insights
+ */
+
+import {execSync} from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import {glob} from 'glob';
+import * as yaml from 'js-yaml';
+import matter from 'gray-matter';
+import {
+ AssetInventoryItem,
+ AssetType,
+ ScreenshotConfigEntry,
+ ScreenshotMapEntry,
+ loadPipelineConfig,
+} from './lib/types';
+
+// ── Regex patterns for extracting references from MDX ──
+
+// Markdown images:  or 
+const MD_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+\.(?:png|jpg|jpeg|gif|webp|svg))\)/gi;
+
+// JSX
tags:
or
+const JSX_IMG_RE = /
]*src=["']([^"']+\.(?:png|jpg|jpeg|gif|webp|svg))["'][^>]*\/?>/gi;
+
+// Arcade embeds:
+const ARCADE_COMPONENT_RE = //gi;
+
+// Commented-out Arcade embeds: {/* */}
+const ARCADE_COMMENTED_RE = /\{\/\*\s*.*?\*\/\s*\}/gi;
+
+// iframe Arcade embeds (legacy):