Skip to content

Commit 7f971f7

Browse files
a-haritigetsantry[bot]chargome
authored
Lint 404s (#10814)
* 404 linter script * 404 linter github workflow * temporarily run linter it's own PR branch * fix file path * skip run if both docs (user facing and dev docs) skip build * fix wording * run a a prod server on localhost before 404 linting * fix commands * fix job name * change * dedupe job name * add progress bar * [getsentry/action-github-commit] Auto commit * use cli-progress for the prgoress bar * add --progress flag to avoid stdout spam * fix: update remaining 404s (#10853) * docs: add section for custom otel sampler (#10843) * fix: remaining 404s * fix 404 * chagne report working * remove unused dependency * remove phony change * switch target branch to master --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com> Co-authored-by: Charly Gomez <[email protected]>
1 parent 3941035 commit 7f971f7

File tree

10 files changed

+253
-10
lines changed

10 files changed

+253
-10
lines changed

.github/workflows/lint-404s.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Lint Docs for 404s
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
index:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- uses: getsentry/action-setup-volta@c52be2ea13cfdc084edb806e81958c13e445941e # v1.2.0
15+
- uses: dorny/paths-filter@v3
16+
id: filter
17+
with:
18+
filters: |
19+
docs:
20+
- 'docs/**'
21+
- 'includes/**'
22+
- 'platform-includes/**'
23+
dev-docs:
24+
- 'develop-docs/**'
25+
- uses: oven-sh/setup-bun@v1
26+
with:
27+
bun-version: latest
28+
29+
- uses: actions/cache@v4
30+
id: cache
31+
with:
32+
path: ${{ github.workspace }}/node_modules
33+
key: node-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
34+
35+
- run: yarn install --frozen-lockfile
36+
if: steps.cache.outputs.cache-hit != 'true'
37+
38+
# Remove the changelog directory to avoid a build error due to missing `DATABASE_URL`
39+
# and save some build time.
40+
- run: rm -r app/changelog
41+
42+
- run: yarn build
43+
if: steps.filter.outputs.docs == 'true'
44+
45+
- run: yarn build:developer-docs
46+
if: steps.filter.outputs.dev-docs == 'true'
47+
48+
- name: Start Http Server
49+
run: yarn start &
50+
if: steps.filter.outputs.docs == 'true' || steps.filter.outputs.dev-docs == 'true'
51+
52+
- name: Lint 404s
53+
run: bun ./scripts/lint-404s/main.ts
54+
if: steps.filter.outputs.docs == 'true' || steps.filter.outputs.dev-docs == 'true'
55+
56+
- name: Kill Http Server
57+
run: kill $(lsof -t -i:3000) || true
58+
if: steps.filter.outputs.docs == 'true' || steps.filter.outputs.dev-docs == 'true'
59+
continue-on-error: true

docs/platforms/apple/common/features/experimental-features.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ description: "Learn about the experimental features available for Sentry's Apple
77
Do you want to try some new experimental features? On the latest version of the Apple SDK, you can:
88

99
- Enable <PlatformLink to="/tracing/instrumentation/automatic-instrumentation/#time-to-full-display">Time to Full Display (TTFD)</PlatformLink> to gain insight into how long it takes your view controller to launch and load all of its content.
10+
<PlatformSection notSupported={["apple.tvos", "apple.watchos", "apple.visionos"]}>
1011
- Enable <PlatformLink to="/profiling/#enable-launch-profiling">App Launch Profiling</PlatformLink> to get detailed profiles for your app launches.
12+
</PlatformSection>
1113
- If you use Swift concurrency, stitch together stack traces of your async code with the `swiftAsyncStacktraces` option. Note that you can enable this in your Objective-C project, but only async code written in Swift will be stitched together.
1214

13-
1415
<Note>
15-
Experimental features are still a work-in-progress and may have bugs. We recognize the irony.
16+
Experimental features are still a work-in-progress and may have bugs. We
17+
recognize the irony.
1618
</Note>
1719

1820
```swift {tabTitle:Swift}

docs/platforms/javascript/guides/aws-lambda/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Depending on your setup, there are different ways to install and use Sentry in y
2222
- [Install the Sentry AWS Lambda Layer](./install/cjs-layer) if your Lambda functions are written in CommonJS (CJS) using `require` syntax.
2323
- [Install the Sentry AWS NPM package](./install/esm-npm) if your Lambda functions are running in EcmaScript Modules (ESM) using `import` syntax.
2424

25-
If you're not sure which installation method to use or want an overview of all available options to use Sentry in your Lambda functions, read the [installation methods overview](/guides/aws-lambda/install).
25+
If you're not sure which installation method to use or want an overview of all available options to use Sentry in your Lambda functions, read the [installation methods overview](./install).
2626

2727
## Configuration
2828

docs/platforms/javascript/guides/aws-lambda/install/cjs-layer.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: "Learn how to add the Sentry Node Lambda Layer to use Sentry in you
44
sidebar_order: 1
55
---
66

7-
The easiest way to get started with Sentry is to use the Sentry [Lambda Layer](https://docs.aws.amazon.com/Lambda/latest/dg/configuration-layers.html) instead of adding `@sentry/aws-serverless` with `npm` or `yarn` [manually](../cjs-manual).
7+
The easiest way to get started with Sentry is to use the Sentry [Lambda Layer](https://docs.aws.amazon.com/Lambda/latest/dg/configuration-layers.html) instead of adding `@sentry/aws-serverless` with `npm` or `yarn` [manually](../cjs-npm).
88
If you follow this guide, you don't have to worry about deploying Sentry dependencies alongside your function code.
99
To actually start the SDK, you can decide between setting up the SDK using environment variables or in your Lambda function code. We recommend using environment variables as it's the easiest way to get started. [Initializing the SDK in code](#alternative-initialize-the-sdk-in-code) instead of setting environment variables gives you more control over the SDK setup if you need it.
1010

docs/platforms/php/common/tracing/instrumentation/caches-module.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ For detailed information about which data can be set, see the [Cache Module deve
1515

1616
## Custom Instrumentation
1717

18-
If you're using anything other than our <PlatformLink to="/guides/laravel/">Laravel SDK</PlatformLink>, you'll need to manually instrument the [Cache Module](https://sentry.io/orgredirect/organizations/:orgslug/performance/caches/) by following the steps below.
18+
If you're using anything other than our [Laravel SDK](/platforms/php/guides/laravel/), you'll need to manually instrument the [Cache Module](https://sentry.io/orgredirect/organizations/:orgslug/performance/caches/) by following the steps below.
1919

2020
### Add Span When Putting Data Into the Cache
2121

docs/product/issues/issue-details/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ It's the most important piece of information that the Sentry grouping algorithm
105105

106106
You can set your own <PlatformLink to="/enriching-events/breadcrumbs/">breadcrumbs</PlatformLink> to make them more useful for debugging.
107107

108-
If you’ve enabled [Session Replay](/product/explore/session-replay/), you’ll see a replay preview under Breadcrumbs if there’s one associated with the event you’re viewing. Replays can be associated with both frontend and [backend errors](/product/explore/session-replay/getting-started#replays-for-backend-errors) (as long as distrubted tracing is set up). Clicking on the replay preview will lead you to the [Replay Details](/product/explore/session-replay/replay-details/) page.
108+
If you’ve enabled [Session Replay](/product/explore/session-replay/), you’ll see a replay preview under Breadcrumbs if there’s one associated with the event you’re viewing. Replays can be associated with both frontend and [backend errors](/product/explore/session-replay/getting-started#replays-for-backend-errors) (as long as distrubted tracing is set up). Clicking on the replay preview will lead you to the [Replay Details](/product/explore/session-replay/web/replay-details/) page.
109109

110110
## Tags
111111

docs/product/performance/transaction-summary.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Spans with the same operation and description are grouped together into a single
157157

158158
### Replays
159159

160-
The Replays tab displays a list of replays where the transaction you’re viewing had occurred. Go directly to [Replay Details](/product/explore/session-replay/replay-details/) for any replay and see how a slow transaction impacted the user experience. Note: you must have [Session Replay](/product/explore/session-replay/) enabled to see this tab.
160+
The Replays tab displays a list of replays where the transaction you’re viewing had occurred. Go directly to [Replay Details](/product/explore/session-replay/web/replay-details/) for any replay and see how a slow transaction impacted the user experience. Note: you must have [Session Replay](/product/explore/session-replay/) enabled to see this tab.
161161

162162
## Additional Actions
163163

scripts/lint-404s/ignore-list.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/
2+
/changelog/

scripts/lint-404s/main.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* eslint-disable no-console */
2+
3+
import {readFileSync} from 'fs';
4+
import path, {dirname} from 'path';
5+
import {fileURLToPath} from 'url';
6+
7+
const baseURL = 'http://localhost:3000/';
8+
type Link = {href: string; innerText: string};
9+
10+
const trimSlashes = (s: string) => s.replace(/(^\/|\/$)/g, '');
11+
12+
// @ts-ignore
13+
const ignoreListFile = path.join(dirname(import.meta.url), './ignore-list.txt');
14+
15+
const showProgress = process.argv.includes('--progress');
16+
17+
// Paths to skip
18+
const ignoreList: string[] = readFileSync(fileURLToPath(ignoreListFile), 'utf8')
19+
.split('\n')
20+
.map(trimSlashes)
21+
.filter(Boolean);
22+
23+
async function fetchWithFollow(url: URL | string): Promise<Response> {
24+
const r = await fetch(url);
25+
if (r.status >= 300 && r.status < 400 && r.headers.has('location')) {
26+
return fetchWithFollow(r.headers.get('location')!);
27+
}
28+
return r;
29+
}
30+
31+
async function main() {
32+
const sitemap = await fetch(`${baseURL}sitemap.xml`).then(r => r.text());
33+
34+
const slugs = [...sitemap.matchAll(/<loc>([^<]*)<\/loc>/g)]
35+
.map(l => l[1])
36+
.map(url => trimSlashes(new URL(url).pathname))
37+
.filter(Boolean);
38+
const allSlugsSet = new Set(slugs);
39+
40+
console.log('Checking 404s on %d pages', slugs.length);
41+
42+
const all404s: {page404s: Link[]; slug: string}[] = [];
43+
44+
// check if the slug equivalent of the href is in the sitemap
45+
const isInSitemap = (href: string) => {
46+
// remove hash
47+
const pathnameSlug = trimSlashes(href.replace(/#.*$/, ''));
48+
49+
// some #hash links result in empty slugs when stripped
50+
return pathnameSlug === '' || allSlugsSet.has(pathnameSlug);
51+
};
52+
53+
function shoudlSkipLink(href: string) {
54+
const isExternal = (href_: string) =>
55+
href_.startsWith('http') || href_.startsWith('mailto:');
56+
const isLocalhost = (href_: string) =>
57+
href_.startsWith('http') && new URL(href_).hostname === 'localhost';
58+
const isIp = (href_: string) => /(\d{1,3}\.){3}\d{1,3}/.test(href_);
59+
const isImage = (href_: string) => /\.(png|jpg|jpeg|gif|svg|webp)$/.test(href_);
60+
61+
return [
62+
isExternal,
63+
(s = '') => ignoreList.includes(trimSlashes(s)),
64+
isImage,
65+
isLocalhost,
66+
isIp,
67+
].some(fn => fn(href));
68+
}
69+
70+
async function is404(link: Link, pageUrl: URL): Promise<boolean> {
71+
if (shoudlSkipLink(link.href)) {
72+
return false;
73+
}
74+
75+
const fullPath = link.href.startsWith('/')
76+
? trimSlashes(link.href)
77+
: // relative path
78+
trimSlashes(new URL(pageUrl.pathname + '/' + link.href, baseURL).pathname);
79+
80+
if (isInSitemap(fullPath)) {
81+
return false;
82+
}
83+
const fullUrl = new URL(fullPath, baseURL);
84+
const resp = await fetchWithFollow(fullUrl);
85+
if (resp.status === 404) {
86+
return true;
87+
}
88+
return false;
89+
}
90+
91+
for (const slug of slugs) {
92+
const pageUrl = new URL(slug, baseURL);
93+
const now = performance.now();
94+
const html = await fetchWithFollow(pageUrl.href).then(r => r.text());
95+
96+
const linkRegex = /<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/g;
97+
const links = Array.from(html.matchAll(linkRegex)).map(m => {
98+
const [, href, innerText] = m;
99+
return {href, innerText};
100+
});
101+
const page404s = (
102+
await Promise.all(
103+
links.map(async link => {
104+
const is404_ = await is404(link, pageUrl);
105+
return [link, is404_] as [Link, boolean];
106+
})
107+
)
108+
)
109+
.filter(([_, is404_]) => is404_)
110+
.map(([link]) => link);
111+
112+
if (page404s.length) {
113+
all404s.push({slug, page404s});
114+
}
115+
116+
if (showProgress) {
117+
console.log(
118+
page404s.length ? '❌' : '✅',
119+
`in ${(performance.now() - now).toFixed(1).padStart(4, '0')} ms | ${slug}`
120+
);
121+
}
122+
}
123+
124+
if (all404s.length === 0) {
125+
console.log('\n\n🎉 No 404s found');
126+
return false;
127+
}
128+
const numberOf404s = all404s.map(x => x.page404s.length).reduce((a, b) => a + b, 0);
129+
console.log(
130+
'\n❌ Found %d %s across %d %s',
131+
numberOf404s,
132+
numberOf404s === 1 ? '404' : '404s',
133+
all404s.length,
134+
all404s.length === 1 ? 'page' : 'pages'
135+
);
136+
for (const {slug, page404s} of all404s) {
137+
console.log('\n🌐', baseURL + slug);
138+
for (const link of page404s) {
139+
console.log(` - [${link.innerText}](${link.href})`);
140+
}
141+
}
142+
143+
console.log(
144+
'\n👉 Note: the markdown syntax is not necessarily present on the source files, but the links do exist on the final pages'
145+
);
146+
// signal error
147+
return true;
148+
}
149+
const now = performance.now();
150+
main().then(has404s => {
151+
console.log(`\n Done in ${(performance.now() - now).toFixed(1)} ms`);
152+
process.exit(has404s ? 1 : 0);
153+
});
154+
155+
export {};

yarn.lock

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10741,7 +10741,16 @@ string-length@^4.0.1:
1074110741
char-regex "^1.0.2"
1074210742
strip-ansi "^6.0.0"
1074310743

10744-
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
10744+
"string-width-cjs@npm:string-width@^4.2.0":
10745+
version "4.2.3"
10746+
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
10747+
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
10748+
dependencies:
10749+
emoji-regex "^8.0.0"
10750+
is-fullwidth-code-point "^3.0.0"
10751+
strip-ansi "^6.0.1"
10752+
10753+
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
1074510754
version "4.2.3"
1074610755
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
1074710756
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10827,7 +10836,14 @@ stringify-entities@^4.0.0:
1082710836
character-entities-html4 "^2.0.0"
1082810837
character-entities-legacy "^3.0.0"
1082910838

10830-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
10839+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
10840+
version "6.0.1"
10841+
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
10842+
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
10843+
dependencies:
10844+
ansi-regex "^5.0.1"
10845+
10846+
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
1083110847
version "6.0.1"
1083210848
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
1083310849
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11840,7 +11856,16 @@ wordwrap@^1.0.0:
1184011856
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
1184111857
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
1184211858

11843-
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
11859+
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
11860+
version "7.0.0"
11861+
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
11862+
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
11863+
dependencies:
11864+
ansi-styles "^4.0.0"
11865+
string-width "^4.1.0"
11866+
strip-ansi "^6.0.0"
11867+
11868+
wrap-ansi@^7.0.0:
1184411869
version "7.0.0"
1184511870
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
1184611871
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

0 commit comments

Comments
 (0)