Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"sentry": {
"type": "http",
"url": "https://mcp.sentry.dev/mcp"
}
}
}
16 changes: 8 additions & 8 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
"@emotion/react": "^11.14.0",
"@sentry/tanstackstart-react": "^10.51.0",
"@tanstack/react-devtools": "^0.9.13",
"@tanstack/react-query": "^5.100.6",
"@tanstack/react-query-devtools": "^5.100.6",
"@tanstack/react-router": "^1.168.26",
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/react-router-ssr-query": "^1.166.12",
"@tanstack/react-start": "^1.167.52",
"@tanstack/router-plugin": "^1.167.29",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-query-devtools": "^5.101.4",
"@tanstack/react-router": "^1.170.18",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/react-router-ssr-query": "^1.167.1",
Comment thread
drav0011 marked this conversation as resolved.
"@tanstack/react-start": "^1.168.32",
"@tanstack/router-plugin": "^1.168.23",
"cross-env": "^10.1.0",
"dotenv-cli": "^11.0.0",
"lucide-react": "^0.562.0",
Expand All @@ -41,7 +41,7 @@
"@chakra-ui/cli": "^3.35.0",
"@sentry/vite-plugin": "^4.9.1",
"@tanstack/devtools-vite": "^0.4.1",
"@tanstack/eslint-plugin-query": "^5.100.6",
"@tanstack/eslint-plugin-query": "^5.101.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.6.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/pack-selection/SelectedPacks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { usePackSelection } from '@/contexts/PackSelectionContext';
import { CategorySelection, DownloadRequest, SECTION_NAME_MAP } from '@/models';
import { Button, Input, Link } from '@/theming/components';
import { generatePackName } from '@/utils/packs';
import { generatePackName, resolveDownloadFileName } from '@/utils/packs';
import {
Box,
CloseButton,
Expand Down Expand Up @@ -52,7 +52,7 @@ export function SelectedPacks({ compatibleVersions, onDownload, onClose }: Selec
const a = document.createElement('a');

a.href = response.downloadUrl;
a.download = new URL(response.downloadUrl).pathname.split('/').pop() ?? response.packName;
a.download = resolveDownloadFileName(response.downloadUrl, response.packName);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
Expand Down
77 changes: 77 additions & 0 deletions apps/web/src/config/sentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Sentry configuration
* Central place to manage which browser errors are worth reporting.
*
* The site embeds AdSense and is visited with all kinds of extensions and
* in-app webviews injected into the page. Those scripts throw constantly and
* Sentry attributes the failures to us, which buries the errors we can act on.
*/

import type { ErrorEvent } from '@sentry/tanstackstart-react';
Comment thread
drav0011 marked this conversation as resolved.

/**
* Error messages that never originate from our own code.
*/
export const SENTRY_IGNORE_ERRORS: (string | RegExp)[] = [
// Google AdSense internals
'Accessing domItems after disposal',
'__tcfapiCall',
/googlesyndication\.com/,
/contentDocument\.body/,

// Extensions and injected globals
/\b(LIDNotify|xbrowser|swbrowser)\b is not defined/,
/window\.ethereum/,
'Invalid call to runtime.sendMessage',
'WKWebView API client did not respond to this postMessage',

// Opaque cross-origin failures with no recoverable detail
'Non-Error promise rejection captured',
'ResizeObserver loop completed with undelivered notifications',
'ResizeObserver loop limit exceeded',
];

/**
* Script origins we never want to attribute errors to.
*/
export const SENTRY_DENY_URLS: RegExp[] = [
// Ad and analytics providers
/googlesyndication\.com/,
/googletagservices\.com/,
/googletagmanager\.com/,
/google-analytics\.com/,
/doubleclick\.net/,
/\/pagead\//,

// Affiliate banner
/bisecthosting\.com/,

// Browser extensions
/^chrome-extension:\/\//,
/^moz-extension:\/\//,
/^safari-(web-)?extension:\/\//,
/^webkit-masked-url:/,
];

/**
* Drop events that carry no usable stack frames.
*
* These are the cross-origin `Script error.` class: a message, no file, no
* line we can map back to a release. They are unactionable by definition, and
* on this site they come from injected third-party scripts.
* @param event - Event Sentry is about to send
* @returns The event to send, or null to drop it
*/
export function dropUnactionableEvent(event: ErrorEvent): ErrorEvent | null {
const values = event.exception?.values;

if (!values?.length) {
return event;
}

const hasUsableFrame = values.some(value =>
value.stacktrace?.frames?.some(frame => Boolean(frame.filename) && frame.filename !== '<anonymous>'),
);

return hasUsableFrame ? event : null;
}
98 changes: 49 additions & 49 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,41 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as TermsRouteImport } from './routes/terms'
import { Route as ResourcePacksRouteImport } from './routes/resource-packs'
import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as CraftingTweaksRouteImport } from './routes/crafting-tweaks'
import { Route as AddonsRouteImport } from './routes/addons'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AddonsRouteImport } from './routes/addons'
import { Route as CraftingTweaksRouteImport } from './routes/crafting-tweaks'
import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as ResourcePacksRouteImport } from './routes/resource-packs'
import { Route as TermsRouteImport } from './routes/terms'

const TermsRoute = TermsRouteImport.update({
id: '/terms',
path: '/terms',
getParentRoute: () => rootRouteImport,
} as any)
const ResourcePacksRoute = ResourcePacksRouteImport.update({
id: '/resource-packs',
path: '/resource-packs',
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PrivacyRoute = PrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
const AddonsRoute = AddonsRouteImport.update({
id: '/addons',
path: '/addons',
getParentRoute: () => rootRouteImport,
} as any)
const CraftingTweaksRoute = CraftingTweaksRouteImport.update({
id: '/crafting-tweaks',
path: '/crafting-tweaks',
getParentRoute: () => rootRouteImport,
} as any)
const AddonsRoute = AddonsRouteImport.update({
id: '/addons',
path: '/addons',
const PrivacyRoute = PrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
const ResourcePacksRoute = ResourcePacksRouteImport.update({
id: '/resource-packs',
path: '/resource-packs',
getParentRoute: () => rootRouteImport,
} as any)
const TermsRoute = TermsRouteImport.update({
id: '/terms',
path: '/terms',
getParentRoute: () => rootRouteImport,
} as any)

Expand Down Expand Up @@ -110,25 +110,18 @@ export interface RootRouteChildren {

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/terms': {
id: '/terms'
path: '/terms'
fullPath: '/terms'
preLoaderRoute: typeof TermsRouteImport
parentRoute: typeof rootRouteImport
}
'/resource-packs': {
id: '/resource-packs'
path: '/resource-packs'
fullPath: '/resource-packs'
preLoaderRoute: typeof ResourcePacksRouteImport
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/privacy': {
id: '/privacy'
path: '/privacy'
fullPath: '/privacy'
preLoaderRoute: typeof PrivacyRouteImport
'/addons': {
id: '/addons'
path: '/addons'
fullPath: '/addons'
preLoaderRoute: typeof AddonsRouteImport
parentRoute: typeof rootRouteImport
}
'/crafting-tweaks': {
Expand All @@ -138,18 +131,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CraftingTweaksRouteImport
parentRoute: typeof rootRouteImport
}
'/addons': {
id: '/addons'
path: '/addons'
fullPath: '/addons'
preLoaderRoute: typeof AddonsRouteImport
'/privacy': {
id: '/privacy'
path: '/privacy'
fullPath: '/privacy'
preLoaderRoute: typeof PrivacyRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
'/resource-packs': {
id: '/resource-packs'
path: '/resource-packs'
fullPath: '/resource-packs'
preLoaderRoute: typeof ResourcePacksRouteImport
parentRoute: typeof rootRouteImport
}
'/terms': {
id: '/terms'
path: '/terms'
fullPath: '/terms'
preLoaderRoute: typeof TermsRouteImport
parentRoute: typeof rootRouteImport
}
}
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InternalServerErrorPage, NotFoundPage } from '@/components/Error';
import { SENTRY_DENY_URLS, SENTRY_IGNORE_ERRORS, dropUnactionableEvent } from '@/config/sentry';
import * as Sentry from '@sentry/tanstackstart-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRouter } from '@tanstack/react-router';
Expand Down Expand Up @@ -30,9 +31,17 @@ export const getRouter = () => {
if (!router.isServer) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
integrations: integrations => [
// BrowserApiErrors wraps addEventListener so third-party listeners throw
// through our Sentry wrapper and get reported as ours. AdSense alone
// accounts for most of our error volume this way. The global onerror and
// onunhandledrejection handlers still catch everything we actually own.
...integrations.filter(integration => integration.name !== 'BrowserApiErrors'),
Sentry.tanstackRouterBrowserTracingIntegration(router),
],
ignoreErrors: SENTRY_IGNORE_ERRORS,
denyUrls: SENTRY_DENY_URLS,
beforeSend: dropUnactionableEvent,
});
}

Expand Down
32 changes: 17 additions & 15 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,23 @@ function RootDocument({ children }: { children: React.ReactNode }): JSX.Element
</AdSenseProvider>
</ChakraProvider>

<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
{
name: 'Tanstack Query',
render: <ReactQueryDevtoolsPanel />,
},
]}
/>
{import.meta.env.DEV && (
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
{
name: 'Tanstack Query',
render: <ReactQueryDevtoolsPanel />,
},
]}
/>
)}
<Scripts />
</body>
</html>
Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/utils/packs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { resolveDownloadFileName } from '@/utils/packs';
import { describe, expect, it } from 'vitest';

describe('resolveDownloadFileName', () => {
it('takes the file name from an absolute URL', () => {
expect(resolveDownloadFileName('https://bedrocktweaks.net/download/abc123/BTRP-042.mcpack', 'fallback'))
.toBe('BTRP-042.mcpack');
});

it('resolves a relative URL against the current page', () => {
expect(resolveDownloadFileName('/download/abc123/BTRP-042.mcpack', 'fallback'))
.toBe('BTRP-042.mcpack');
});

it('decodes percent-encoded file names', () => {
expect(resolveDownloadFileName('https://bedrocktweaks.net/download/abc123/My%20Pack.mcpack', 'fallback'))
.toBe('My Pack.mcpack');
});

it('keeps the raw segment when percent-encoding is malformed', () => {
expect(resolveDownloadFileName('https://bedrocktweaks.net/download/abc123/100%.mcpack', 'fallback'))
.toBe('100%.mcpack');
});

it('strips query strings and fragments', () => {
expect(resolveDownloadFileName('https://bedrocktweaks.net/download/abc123/BTRP-042.mcpack?t=1#top', 'fallback'))
.toBe('BTRP-042.mcpack');
});

// The regression behind BT-REACT-3: these used to throw out of the mutation's
// onSuccess callback, which aborted the click that starts the download.
it.each([
['an unparseable URL', 'not a url at all'],
['a malformed protocol', 'https,http://bedrocktweaks.net/download/abc/x.mcpack'],
['an empty string', ''],
['undefined', undefined],
])('does not throw for %s', (_label, downloadUrl) => {
expect(() => resolveDownloadFileName(downloadUrl, 'fallback')).not.toThrow();
});

it('falls back to the pack name when no file name can be recovered', () => {
expect(resolveDownloadFileName(undefined, 'BTRP-042')).toBe('BTRP-042');
expect(resolveDownloadFileName('', 'BTRP-042')).toBe('BTRP-042');
expect(resolveDownloadFileName('https://bedrocktweaks.net/download/', 'BTRP-042')).toBe('BTRP-042');
});
});
Loading