From 9beff8678f884d9a110d9ef6614f86c60e5cdd7e Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 13:58:14 +0200 Subject: [PATCH 01/17] Sentry Issues Signed-off-by: prxt6529 --- AGENTS.md | 44 ++++++++++++++ components/waves/CreateDropStormParts.tsx | 27 ++++++--- .../hooks/useWaveDropsClipboard.ts | 2 +- instrumentation-client.ts | 37 ++++++++++-- services/api/common-api.ts | 59 ++++++++++++++----- 5 files changed, 139 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae80cf16d3..c5533592bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,50 @@ Enable the **Next DevTools MCP server** so agents can query live routes, errors, --- +## Sentry Error Handling + +When addressing issues reported by Sentry, **always attempt to fix the root cause first**. Silencing errors should only be considered as a last resort when fixing is genuinely not possible. + +### Default Workflow + +1. **Fix the code to prevent the issue** (primary action) + * Analyze the error stack trace and identify the root cause + * Implement a proper fix that addresses the underlying problem + * Add appropriate error handling, validation, or defensive checks + * Update tests to cover the fix + +2. **Ask about silencing only if fixing is not possible** (fallback) + * Only proceed to silencing if the error is genuinely unfixable, such as: + * Third-party library errors that cannot be patched + * Browser extension noise (e.g., injected scripts) + * Known browser bugs that cannot be worked around + * Expected errors that are already handled gracefully in the UI + +### Where Silencing Happens + +If silencing is necessary, errors are filtered in the `beforeSend` callback of Sentry initialization: + +* **Client-side**: [`instrumentation-client.ts`](instrumentation-client.ts) — contains `noisyPatterns`, `referenceErrors`, and `filenameExceptions` arrays +* **Server-side**: [`sentry.server.config.ts`](sentry.server.config.ts) — server runtime configuration +* **Edge runtime**: [`sentry.edge.config.ts`](sentry.edge.config.ts) — edge runtime configuration + +### Examples + +**Fixable (default action):** +* Null reference errors → add null checks or optional chaining +* Type errors → fix type definitions or add runtime validation +* Network errors → implement retry logic or better error handling +* Missing dependencies → add proper dependency arrays or fix imports + +**Non-fixable (ask about silencing):** +* Browser extension injecting scripts (`inpage.js` errors) +* Known browser bugs (e.g., `ResizeObserver loop limit exceeded` in some browsers) +* Third-party library errors that cannot be patched without forking + +This aligns with the "Fix with modernization" principle: prioritize meaningful fixes over suppressing symptoms. + +--- + ## Next.js 16: What this means for agents * **Proxy instead of Middleware:** `middleware.ts` is **renamed to** `proxy.ts` (Node runtime). If you touch request‑boundary logic, ensure the file and exported function are named `proxy`. Legacy `middleware.ts` still exists for edge‑only cases but our default is `proxy.ts`. ([Next.js][6]) diff --git a/components/waves/CreateDropStormParts.tsx b/components/waves/CreateDropStormParts.tsx index 5f8c439fdf..88721750fd 100644 --- a/components/waves/CreateDropStormParts.tsx +++ b/components/waves/CreateDropStormParts.tsx @@ -1,16 +1,16 @@ "use client"; -import React from "react"; import { CreateDropPart, ReferencedNft } from "@/entities/IDrop"; import { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser"; -import { AuthContext } from "../auth/Auth"; import { cicToType } from "@/helpers/Helpers"; -import Link from "next/link"; -import CreateDropStormPart from "./CreateDropStormPart"; import { AnimatePresence, motion } from "framer-motion"; +import Link from "next/link"; +import React from "react"; +import { AuthContext } from "../auth/Auth"; import UserCICAndLevel, { UserCICAndLevelSize, } from "../user/utils/UserCICAndLevel"; +import CreateDropStormPart from "./CreateDropStormPart"; interface CreateDropStormPartsProps { parts: CreateDropPart[]; @@ -28,6 +28,15 @@ const CreateDropStormParts: React.FC = ({ const { connectedProfile } = React.useContext(AuthContext); const cicType = cicToType(connectedProfile?.cic ?? 0); + const partKeys = React.useMemo(() => { + return parts.map((part, index) => { + const stableId = part.quoted_drop + ? `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}` + : `content-${index}-${part.media.length}`; + return stableId; + }); + }, [parts]); + return (
@@ -61,11 +70,11 @@ const CreateDropStormParts: React.FC = ({
- - +
+ {parts.map((part, partIndex) => ( = ({ /> ))} - - + +
diff --git a/components/waves/drops/wave-drops-all/hooks/useWaveDropsClipboard.ts b/components/waves/drops/wave-drops-all/hooks/useWaveDropsClipboard.ts index 4e090676f4..e68791c8ef 100644 --- a/components/waves/drops/wave-drops-all/hooks/useWaveDropsClipboard.ts +++ b/components/waves/drops/wave-drops-all/hooks/useWaveDropsClipboard.ts @@ -1122,7 +1122,7 @@ export const useWaveDropsClipboard = ({ } const handleKeyDown = (event: KeyboardEvent) => { - if (event.key.toLowerCase() !== "c") { + if (typeof event.key !== "string" || event.key.toLowerCase() !== "c") { return; } diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 5857046625..ec9d0f31b8 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -13,6 +13,16 @@ const sentryEnabled = !!publicEnv.SENTRY_DSN; const isProduction = publicEnv.NODE_ENV === "production"; const dsn = publicEnv.SENTRY_DSN; +const noisyPatterns = [ + "EmptyRanges", + "ResizeObserver loop limit exceeded", + "Non-Error promise rejection captured", +]; + +const referenceErrors = ["__firefox__"]; + +const filenameExceptions = ["inpage.js"]; + Sentry.init({ dsn: sentryEnabled ? dsn : undefined, enabled: sentryEnabled, @@ -40,17 +50,32 @@ Sentry.init({ } const message = value?.value ?? fallbackMessage; + const errorType = value?.type; if (typeof message === "string") { - const noisyPatterns = [ - "EmptyRanges", - "ResizeObserver loop limit exceeded", - "Non-Error promise rejection captured", - ]; - if (noisyPatterns.some((p) => message.includes(p))) { return null; } + + if ( + (errorType === "ReferenceError" || errorType === "TypeError") && + referenceErrors.some((p) => message.includes(p)) + ) { + return null; + } + } + + const frames = event.exception?.values?.[0]?.stacktrace?.frames; + if ( + frames?.some((frame: any) => + filenameExceptions.some( + (pattern) => + frame?.filename?.includes(pattern) || + frame?.abs_path?.includes(pattern) + ) + ) + ) { + return null; } const error = hint.originalException || hint.syntheticException; diff --git a/services/api/common-api.ts b/services/api/common-api.ts index 0c81255ebf..4319687f52 100644 --- a/services/api/common-api.ts +++ b/services/api/common-api.ts @@ -63,22 +63,53 @@ const executeApiRequest = async ( signal?: AbortSignal, parseJson: boolean = true ): Promise => { - const res = await fetch(url, { - method, - headers, - body, - signal, - }); - - if (!res.ok) { - return handleApiError(res); - } + try { + const res = await fetch(url, { + method, + headers, + body, + signal, + }); - if (!parseJson) { - return undefined as T; - } + if (!res.ok) { + return handleApiError(res); + } - return res.json(); + if (!parseJson) { + return undefined as T; + } + + try { + return await res.json(); + } catch (jsonError) { + throw new Error( + `Failed to parse response as JSON from ${url}: ${ + jsonError instanceof Error ? jsonError.message : String(jsonError) + }` + ); + } + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw error; + } + + if (error instanceof TypeError) { + const errorMessage = error.message.toLowerCase(); + if ( + errorMessage.includes("load failed") || + errorMessage.includes("failed to fetch") + ) { + throw new Error( + `Network request failed. Please check your connection and try again. (${url})` + ); + } + if (errorMessage.includes("network")) { + throw new Error(`Network error: ${error.message} (${url})`); + } + } + + throw error; + } }; export const commonApiFetch = async >(param: { From 919494b841cd4519ce5cc5b7d58ce8845d7740a2 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 14:17:02 +0200 Subject: [PATCH 02/17] WIP Signed-off-by: prxt6529 --- components/waves/CreateDropStormParts.tsx | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/components/waves/CreateDropStormParts.tsx b/components/waves/CreateDropStormParts.tsx index 88721750fd..25683d7325 100644 --- a/components/waves/CreateDropStormParts.tsx +++ b/components/waves/CreateDropStormParts.tsx @@ -28,13 +28,30 @@ const CreateDropStormParts: React.FC = ({ const { connectedProfile } = React.useContext(AuthContext); const cicType = cicToType(connectedProfile?.cic ?? 0); + const partIdCounterRef = React.useRef(0); + const partIdsRef = React.useRef([]); + const partKeys = React.useMemo(() => { - return parts.map((part, index) => { - const stableId = part.quoted_drop - ? `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}` - : `content-${index}-${part.media.length}`; - return stableId; + const keys: string[] = []; + + parts.forEach((part, index) => { + if (part.quoted_drop) { + const quotedKey = `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}`; + keys.push(quotedKey); + } else { + if (index < partIdsRef.current.length) { + keys.push(partIdsRef.current[index]); + } else { + const newId = `part-${partIdCounterRef.current++}`; + partIdsRef.current[index] = newId; + keys.push(newId); + } + } }); + + partIdsRef.current = partIdsRef.current.slice(0, keys.length); + + return keys; }, [parts]); return ( From f6626bd1fdf3595f2a810724c55117d26124d4e0 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 14:20:20 +0200 Subject: [PATCH 03/17] WIP Signed-off-by: prxt6529 --- AGENTS.md | 1 + components/waves/CreateDropStormParts.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c5533592bf..0ec7662798 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -226,6 +226,7 @@ If you add or modify `proxy.ts`, keep it at the root (or `src/`) alongside `app/ * Tests live in `__tests__/` or `ComponentName.test.tsx`. * Mock external dependencies and APIs in tests. * When parsing Seize URLs (or similar), **do not** fall back to placeholder origins; fail fast if base origin is unavailable. +* **React imports:** Prefer direct named imports (`useMemo`, `useRef`, `FC`, etc.) over `React.` namespace usage (`React.useMemo`, `React.useRef`, `React.FC`, etc.). Import hooks and types directly: `import { useMemo, useRef, FC, memo } from "react"` rather than `import React from "react"` and using `React.useMemo`. --- diff --git a/components/waves/CreateDropStormParts.tsx b/components/waves/CreateDropStormParts.tsx index 25683d7325..3f469d333a 100644 --- a/components/waves/CreateDropStormParts.tsx +++ b/components/waves/CreateDropStormParts.tsx @@ -5,7 +5,7 @@ import { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser"; import { cicToType } from "@/helpers/Helpers"; import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; -import React from "react"; +import { FC, memo, useContext, useMemo, useRef } from "react"; import { AuthContext } from "../auth/Auth"; import UserCICAndLevel, { UserCICAndLevelSize, @@ -19,19 +19,19 @@ interface CreateDropStormPartsProps { onRemovePart: (partIndex: number) => void; } -const CreateDropStormParts: React.FC = ({ +const CreateDropStormParts: FC = ({ parts, mentionedUsers, referencedNfts, onRemovePart, }) => { - const { connectedProfile } = React.useContext(AuthContext); + const { connectedProfile } = useContext(AuthContext); const cicType = cicToType(connectedProfile?.cic ?? 0); - const partIdCounterRef = React.useRef(0); - const partIdsRef = React.useRef([]); + const partIdCounterRef = useRef(0); + const partIdsRef = useRef([]); - const partKeys = React.useMemo(() => { + const partKeys = useMemo(() => { const keys: string[] = []; parts.forEach((part, index) => { @@ -114,4 +114,4 @@ const CreateDropStormParts: React.FC = ({ ); }; -export default React.memo(CreateDropStormParts); +export default memo(CreateDropStormParts); From f418870f1caa043602df0b572ff58c1c1c97e954 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 14:26:35 +0200 Subject: [PATCH 04/17] WIP Signed-off-by: prxt6529 --- components/waves/CreateDropStormParts.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/components/waves/CreateDropStormParts.tsx b/components/waves/CreateDropStormParts.tsx index 3f469d333a..f89db94bad 100644 --- a/components/waves/CreateDropStormParts.tsx +++ b/components/waves/CreateDropStormParts.tsx @@ -38,14 +38,12 @@ const CreateDropStormParts: FC = ({ if (part.quoted_drop) { const quotedKey = `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}`; keys.push(quotedKey); + } else if (index < partIdsRef.current.length) { + keys.push(partIdsRef.current[index]); } else { - if (index < partIdsRef.current.length) { - keys.push(partIdsRef.current[index]); - } else { - const newId = `part-${partIdCounterRef.current++}`; - partIdsRef.current[index] = newId; - keys.push(newId); - } + const newId = `part-${partIdCounterRef.current++}`; + partIdsRef.current[index] = newId; + keys.push(newId); } }); From 0fed9072cc140aadff762f7265d10719e2085049 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 14:38:50 +0200 Subject: [PATCH 05/17] WIP Signed-off-by: prxt6529 --- components/waves/CreateDropContent.tsx | 47 ++++++++++++++++---------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/components/waves/CreateDropContent.tsx b/components/waves/CreateDropContent.tsx index 1cc23a41ef..635ebc1e8f 100644 --- a/components/waves/CreateDropContent.tsx +++ b/components/waves/CreateDropContent.tsx @@ -609,23 +609,27 @@ const CreateDropContent: React.FC = ({ allMentions: ApiDropMentionedUser[], allNfts: ReferencedNft[] ): CreateDropConfig => { - const parts = ensurePartsWithFallback( - [ - ...(drop?.parts ?? []), - { - content: markdown?.length ? markdown : null, - quoted_drop: - activeDrop?.action === ActiveDropAction.QUOTE - ? { - drop_id: activeDrop.drop.id, - drop_part_id: activeDrop.partId, - } - : null, - media: files, - }, - ], - hasMetadata - ); + const isStormMode = (drop?.parts.length ?? 0) > 0; + const hasCurrentContent = !!(markdown?.trim().length || files.length); + + const newParts = isStormMode && !hasCurrentContent + ? drop?.parts ?? [] + : [ + ...(drop?.parts ?? []), + { + content: markdown?.length ? markdown : null, + quoted_drop: + activeDrop?.action === ActiveDropAction.QUOTE + ? { + drop_id: activeDrop.drop.id, + drop_part_id: activeDrop.partId, + } + : null, + media: files, + }, + ]; + + const parts = ensurePartsWithFallback(newParts, hasMetadata); return { title: null, @@ -869,6 +873,15 @@ const CreateDropContent: React.FC = ({ ) { return; } + + const isStormMode = (drop?.parts.length ?? 0) > 0; + const hasCurrentContent = !!(getMarkdown?.trim().length || files.length); + + if (isStormMode && hasCurrentContent) { + finalizeAndAddDropPart(); + return; + } + await prepareAndSubmitDrop(getUpdatedDrop()); }; From 496a9ee0ecc98dbbbe091b541efe054f2cdba8b2 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 14:58:43 +0200 Subject: [PATCH 06/17] WIP Signed-off-by: prxt6529 --- components/waves/CreateDropContent.tsx | 8 ++-- components/waves/CreateDropStormParts.tsx | 58 ++++++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/components/waves/CreateDropContent.tsx b/components/waves/CreateDropContent.tsx index 635ebc1e8f..25528fbcd9 100644 --- a/components/waves/CreateDropContent.tsx +++ b/components/waves/CreateDropContent.tsx @@ -609,10 +609,10 @@ const CreateDropContent: React.FC = ({ allMentions: ApiDropMentionedUser[], allNfts: ReferencedNft[] ): CreateDropConfig => { - const isStormMode = (drop?.parts.length ?? 0) > 0; + const hasPartsInDrop = (drop?.parts.length ?? 0) > 0; const hasCurrentContent = !!(markdown?.trim().length || files.length); - const newParts = isStormMode && !hasCurrentContent + const newParts = hasPartsInDrop && !hasCurrentContent ? drop?.parts ?? [] : [ ...(drop?.parts ?? []), @@ -874,10 +874,10 @@ const CreateDropContent: React.FC = ({ return; } - const isStormMode = (drop?.parts.length ?? 0) > 0; + const hasPartsInDrop = (drop?.parts.length ?? 0) > 0; const hasCurrentContent = !!(getMarkdown?.trim().length || files.length); - if (isStormMode && hasCurrentContent) { + if (hasPartsInDrop && hasCurrentContent) { finalizeAndAddDropPart(); return; } diff --git a/components/waves/CreateDropStormParts.tsx b/components/waves/CreateDropStormParts.tsx index f89db94bad..f697fe8d99 100644 --- a/components/waves/CreateDropStormParts.tsx +++ b/components/waves/CreateDropStormParts.tsx @@ -5,7 +5,15 @@ import { ApiDropMentionedUser } from "@/generated/models/ApiDropMentionedUser"; import { cicToType } from "@/helpers/Helpers"; import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; -import { FC, memo, useContext, useMemo, useRef } from "react"; +import { + FC, + memo, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { AuthContext } from "../auth/Auth"; import UserCICAndLevel, { UserCICAndLevelSize, @@ -29,29 +37,43 @@ const CreateDropStormParts: FC = ({ const cicType = cicToType(connectedProfile?.cic ?? 0); const partIdCounterRef = useRef(0); - const partIdsRef = useRef([]); + const [partIdsMap, setPartIdsMap] = useState>(new Map()); - const partKeys = useMemo(() => { - const keys: string[] = []; + useEffect(() => { + setPartIdsMap((prevMap) => { + const newMap = new Map(prevMap); + let changed = false; - parts.forEach((part, index) => { - if (part.quoted_drop) { - const quotedKey = `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}`; - keys.push(quotedKey); - } else if (index < partIdsRef.current.length) { - keys.push(partIdsRef.current[index]); - } else { - const newId = `part-${partIdCounterRef.current++}`; - partIdsRef.current[index] = newId; - keys.push(newId); - } - }); + parts.forEach((part, index) => { + if (!part.quoted_drop) { + if (!newMap.has(index)) { + newMap.set(index, `part-${partIdCounterRef.current++}`); + changed = true; + } + } + }); - partIdsRef.current = partIdsRef.current.slice(0, keys.length); + const maxIndex = parts.length - 1; + Array.from(newMap.keys()).forEach((key) => { + if (key > maxIndex) { + newMap.delete(key); + changed = true; + } + }); - return keys; + return changed ? newMap : prevMap; + }); }, [parts]); + const partKeys = useMemo(() => { + return parts.map((part, index) => { + if (part.quoted_drop) { + return `quoted-${part.quoted_drop.drop_id}-${part.quoted_drop.drop_part_id}`; + } + return partIdsMap.get(index) ?? `part-fallback-${index}`; + }); + }, [parts, partIdsMap]); + return (
From 0e7c4248508f5b3182bbf31d35c1663967fbe400 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 15:36:21 +0200 Subject: [PATCH 07/17] WIP Signed-off-by: prxt6529 --- .../NotificationsContext.test.tsx | 117 +++++++++++------- .../waves/CreateDropStormParts.test.tsx | 17 ++- config/sentryProbes.ts | 50 ++++++++ instrumentation-client.ts | 43 +++++++ sentry.edge.config.ts | 13 +- sentry.server.config.ts | 13 +- 6 files changed, 195 insertions(+), 58 deletions(-) diff --git a/__tests__/components/notifications/NotificationsContext.test.tsx b/__tests__/components/notifications/NotificationsContext.test.tsx index cff36f090d..3c9812b93c 100644 --- a/__tests__/components/notifications/NotificationsContext.test.tsx +++ b/__tests__/components/notifications/NotificationsContext.test.tsx @@ -1,9 +1,12 @@ -import React from "react"; -import { renderHook, act } from "@testing-library/react"; import { NotificationsProvider, useNotificationsContext, } from "@/components/notifications/NotificationsContext"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import React from "react"; + +const push = jest.fn(); +const mockUseRouter = jest.fn(() => ({ push })); jest.mock("@/hooks/useCapacitor", () => () => ({ isCapacitor: true, @@ -11,15 +14,36 @@ jest.mock("@/hooks/useCapacitor", () => () => ({ })); jest.mock("next/navigation", () => ({ __esModule: true, - useRouter: jest.fn(() => ({ push: jest.fn() })), + useRouter: () => mockUseRouter(), })); jest.mock("@/components/auth/Auth", () => ({ - useAuth: () => ({ connectedProfile: null }), + useAuth: () => ({ connectedProfile: { id: "test-profile-id" } }), })); jest.mock("@/services/api/common-api", () => ({ + commonApiPost: jest.fn().mockResolvedValue({}), commonApiPostWithoutBodyAndResponse: jest.fn().mockResolvedValue({}), })); +jest.mock("@capacitor/push-notifications", () => { + return { + PushNotifications: { + removeAllListeners: jest.fn().mockResolvedValue(undefined), + addListener: jest.fn(), + requestPermissions: jest.fn().mockResolvedValue({ receive: "granted" }), + register: jest.fn().mockResolvedValue(undefined), + getDeliveredNotifications: jest + .fn() + .mockResolvedValue({ notifications: [{ data: { wave_id: "w1" } }] }), + removeDeliveredNotifications: jest.fn().mockResolvedValue(undefined), + removeAllDeliveredNotifications: jest.fn().mockResolvedValue(undefined), + }, + PushNotificationSchema: {}, + }; +}); +jest.mock("@capacitor/device", () => ({ + Device: { getInfo: jest.fn().mockResolvedValue({ platform: "ios" }) }, +})); + const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); @@ -44,69 +68,66 @@ describe("NotificationsContext", () => { }); }); -jest.mock("@capacitor/push-notifications", () => ({ - PushNotifications: { - removeAllListeners: jest.fn(), - addListener: jest.fn(), - requestPermissions: jest.fn().mockResolvedValue({ receive: "granted" }), - register: jest.fn(), - getDeliveredNotifications: jest - .fn() - .mockResolvedValue({ notifications: [{ data: { wave_id: "w1" } }] }), - removeDeliveredNotifications: jest.fn(), - removeAllDeliveredNotifications: jest.fn(), - }, -})); - -jest.mock("@capacitor/device", () => ({ - Device: { getInfo: jest.fn().mockResolvedValue({ platform: "ios" }) }, -})); - it("removes notifications when functions called", async () => { + const { PushNotifications } = require("@capacitor/push-notifications"); const { result } = renderHook(() => useNotificationsContext(), { wrapper }); + + await waitFor(() => { + expect(PushNotifications.removeAllListeners).toHaveBeenCalled(); + }); + await act(async () => { await result.current.removeWaveDeliveredNotifications("w1"); await result.current.removeAllDeliveredNotifications(); }); - const { PushNotifications } = require("@capacitor/push-notifications"); expect(PushNotifications.getDeliveredNotifications).toHaveBeenCalled(); expect(PushNotifications.removeDeliveredNotifications).toHaveBeenCalled(); expect(PushNotifications.removeAllDeliveredNotifications).toHaveBeenCalled(); }); describe("push notification action handling", () => { + beforeEach(() => { + push.mockClear(); + const { PushNotifications } = require("@capacitor/push-notifications"); + PushNotifications.addListener.mockClear(); + PushNotifications.removeDeliveredNotifications.mockClear(); + }); + it("redirects based on notification data", async () => { - const push = jest.fn(); - jest - .spyOn(require("next/navigation"), "useRouter") - .mockReturnValue({ push } as any); - - const addListenerMock = jest.fn((evt, cb) => { - if (evt === "pushNotificationActionPerformed") { - setTimeout( - () => - cb({ - notification: { - data: { - redirect: "profile", - handle: "abc", - notification_id: "1", - }, - }, - }), - 0 - ); - } + const { PushNotifications } = require("@capacitor/push-notifications"); + const { result } = renderHook(() => useNotificationsContext(), { wrapper }); + + await waitFor(() => { + expect(PushNotifications.addListener).toHaveBeenCalled(); }); - const { PushNotifications } = require("@capacitor/push-notifications"); - PushNotifications.addListener = addListenerMock; + const addListenerCalls = PushNotifications.addListener.mock.calls; + const actionPerformedCall = addListenerCalls.find( + (call: any[]) => call[0] === "pushNotificationActionPerformed" + ); + const callback = actionPerformedCall?.[1]; + + expect(callback).toBeDefined(); - const { result } = renderHook(() => useNotificationsContext(), { wrapper }); await act(async () => { + if (callback) { + await callback({ + notification: { + data: { + redirect: "profile", + handle: "abc", + notification_id: "1", + }, + }, + }); + } await new Promise((r) => setTimeout(r, 100)); }); - expect(push).toHaveBeenCalledWith("/abc"); + + await waitFor(() => { + expect(push).toHaveBeenCalledWith("/abc"); + }); + expect(PushNotifications.removeDeliveredNotifications).toHaveBeenCalled(); }); }); diff --git a/__tests__/components/waves/CreateDropStormParts.test.tsx b/__tests__/components/waves/CreateDropStormParts.test.tsx index 661319c736..6128b2499a 100644 --- a/__tests__/components/waves/CreateDropStormParts.test.tsx +++ b/__tests__/components/waves/CreateDropStormParts.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import CreateDropStormParts from '@/components/waves/CreateDropStormParts'; import { AuthContext } from '@/components/auth/Auth'; @@ -8,12 +8,19 @@ jest.mock('@/components/waves/CreateDropStormPart', () => ({ default: ({ partIndex }: any) =>
, })); +jest.mock('framer-motion', () => ({ + AnimatePresence: ({ children }: any) => children, + motion: { + div: ({ children, ...props }: any) =>
{children}
, + }, +})); + const authValue = { connectedProfile: { handle: 'user', pfp: 'img.png', level: 1, cic: 0 }, } as any; describe('CreateDropStormParts', () => { - it('renders parts with profile info', () => { + it('renders parts with profile info', async () => { const parts = [{ content: 'a' }, { content: 'b' }] as any; render( @@ -25,8 +32,10 @@ describe('CreateDropStormParts', () => { /> ); - expect(screen.getByTestId('part-0')).toBeInTheDocument(); - expect(screen.getByTestId('part-1')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getAllByTestId('part-0')[0]).toBeInTheDocument(); + }); + expect(screen.getAllByTestId('part-1')[0]).toBeInTheDocument(); expect(screen.getByRole('img')).toHaveAttribute('src', 'img.png'); expect(screen.getByRole('link')).toHaveAttribute('href', '/user'); }); diff --git a/config/sentryProbes.ts b/config/sentryProbes.ts index 6e4091552a..377c99555d 100644 --- a/config/sentryProbes.ts +++ b/config/sentryProbes.ts @@ -16,6 +16,56 @@ const probeTags = { probe_type: "generic-exploit-scan", }; +export function filterTunnelRouteErrors(event: any, hint?: any): any | null { + const value = event.exception?.values?.[0]; + const message = value?.value || ""; + const errorType = value?.type || ""; + + if (typeof message === "string") { + if ( + message.includes("aborted") || + message.includes("ECONNRESET") || + message.includes("socket hang up") + ) { + const url = event.request?.url || ""; + const stacktrace = value?.stacktrace?.frames || []; + + const isMonitoringRoute = + url.includes("/monitoring") || + stacktrace.some( + (frame: any) => + frame?.filename?.includes("monitoring") || + frame?.abs_path?.includes("monitoring") + ); + + if (isMonitoringRoute) { + return null; + } + } + } + + if (errorType === "Error" && typeof message === "string") { + if ( + message.includes("aborted") && + hint?.originalException instanceof Error + ) { + const stack = hint.originalException.stack || ""; + if ( + stack.includes("_http_server") || + stack.includes("abortIncoming") || + stack.includes("socketOnClose") + ) { + const url = event.request?.url || ""; + if (url.includes("/monitoring")) { + return null; + } + } + } + } + + return event; +} + export function tagSecurityProbes(event: any) { try { const url = (event?.request?.url || "").toLowerCase(); diff --git a/instrumentation-client.ts b/instrumentation-client.ts index ec9d0f31b8..cab8f3f58b 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -90,6 +90,49 @@ Sentry.init({ event.fingerprint = ["indexeddb-connection-lost"]; } + if (error instanceof TypeError) { + const errorMessage = error.message.toLowerCase(); + if ( + errorMessage.includes("load failed") || + errorMessage.includes("failed to fetch") || + errorMessage.includes("network") + ) { + let url = "unknown"; + const urlMatch = error.message.match(/\(([^)]+)\)/); + if (urlMatch) { + url = urlMatch[1]; + } else { + const fetchBreadcrumb = event.breadcrumbs?.find( + (crumb: any) => crumb.category === "fetch" || crumb.type === "http" + ); + if (fetchBreadcrumb?.data?.url) { + url = fetchBreadcrumb.data.url; + } else if (event.request?.url) { + url = event.request.url; + } + } + + const transformedMessage = errorMessage.includes("network") + ? `Network error: ${error.message} (${url})` + : `Network request failed. Please check your connection and try again. (${url})`; + + if (value) { + value.value = transformedMessage; + } + if (event.message) { + event.message = transformedMessage; + } + + event.level = "warning"; + event.tags = { + ...event.tags, + errorType: "network", + handled: true, + }; + event.fingerprint = ["network-error"]; + } + } + return event; }, }); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index 36a1dc138a..6b45c70c7b 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -5,7 +5,10 @@ // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import { publicEnv } from "@/config/env"; -import { tagSecurityProbes } from "@/config/sentryProbes"; +import { + filterTunnelRouteErrors, + tagSecurityProbes, +} from "@/config/sentryProbes"; import * as Sentry from "@sentry/nextjs"; const dsn = publicEnv.SENTRY_DSN; @@ -26,7 +29,11 @@ Sentry.init({ // ------------------------------------------------------------ // Handle obvious bot / exploit probes more gently (edge) // ------------------------------------------------------------ - beforeSend(event) { - return tagSecurityProbes(event); + beforeSend(event, hint) { + const filtered = filterTunnelRouteErrors(event, hint); + if (filtered === null) { + return null; + } + return tagSecurityProbes(filtered); }, }); diff --git a/sentry.server.config.ts b/sentry.server.config.ts index d3f67b0ea2..9e966ffd03 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -3,7 +3,10 @@ // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import { publicEnv } from "@/config/env"; -import { tagSecurityProbes } from "@/config/sentryProbes"; +import { + filterTunnelRouteErrors, + tagSecurityProbes, +} from "@/config/sentryProbes"; import * as Sentry from "@sentry/nextjs"; const dsn = publicEnv.SENTRY_DSN; @@ -26,7 +29,11 @@ Sentry.init({ // ------------------------------------------------------------ // Handle obvious bot / exploit probes more gently // ------------------------------------------------------------ - beforeSend(event) { - return tagSecurityProbes(event); + beforeSend(event, hint) { + const filtered = filterTunnelRouteErrors(event, hint); + if (filtered === null) { + return null; + } + return tagSecurityProbes(filtered); }, }); From 98b4c98d9ff081dbd914ca911d06237f620fe71a Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 15:44:42 +0200 Subject: [PATCH 08/17] WIP Signed-off-by: prxt6529 --- instrumentation-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-client.ts b/instrumentation-client.ts index cab8f3f58b..41c2ce8098 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -98,8 +98,8 @@ Sentry.init({ errorMessage.includes("network") ) { let url = "unknown"; - const urlMatch = error.message.match(/\(([^)]+)\)/); - if (urlMatch) { + const urlMatch = error.message.match(/\(([^)]+?)\)/); + if (urlMatch && urlMatch[1].length < 2048) { url = urlMatch[1]; } else { const fetchBreadcrumb = event.breadcrumbs?.find( From e7fe42e239a9434a070612df8fb0210ed6512914 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 15:46:58 +0200 Subject: [PATCH 09/17] WIP Signed-off-by: prxt6529 --- instrumentation-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 41c2ce8098..450cab1613 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -98,8 +98,8 @@ Sentry.init({ errorMessage.includes("network") ) { let url = "unknown"; - const urlMatch = error.message.match(/\(([^)]+?)\)/); - if (urlMatch && urlMatch[1].length < 2048) { + const urlMatch = error.message.slice(0, 2048).match(/\(([^)]+?)\)/); + if (urlMatch) { url = urlMatch[1]; } else { const fetchBreadcrumb = event.breadcrumbs?.find( From 6de6b9a328f9f0886d547132498995fed927bcc9 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 15:50:15 +0200 Subject: [PATCH 10/17] WIP Signed-off-by: prxt6529 --- .../waves/CreateDropStormParts.test.tsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/__tests__/components/waves/CreateDropStormParts.test.tsx b/__tests__/components/waves/CreateDropStormParts.test.tsx index 6128b2499a..7510508104 100644 --- a/__tests__/components/waves/CreateDropStormParts.test.tsx +++ b/__tests__/components/waves/CreateDropStormParts.test.tsx @@ -1,14 +1,13 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import React from 'react'; -import CreateDropStormParts from '@/components/waves/CreateDropStormParts'; -import { AuthContext } from '@/components/auth/Auth'; +import { AuthContext } from "@/components/auth/Auth"; +import CreateDropStormParts from "@/components/waves/CreateDropStormParts"; +import { render, screen } from "@testing-library/react"; -jest.mock('@/components/waves/CreateDropStormPart', () => ({ +jest.mock("@/components/waves/CreateDropStormPart", () => ({ __esModule: true, default: ({ partIndex }: any) =>
, })); -jest.mock('framer-motion', () => ({ +jest.mock("framer-motion", () => ({ AnimatePresence: ({ children }: any) => children, motion: { div: ({ children, ...props }: any) =>
{children}
, @@ -16,12 +15,12 @@ jest.mock('framer-motion', () => ({ })); const authValue = { - connectedProfile: { handle: 'user', pfp: 'img.png', level: 1, cic: 0 }, + connectedProfile: { handle: "user", pfp: "img.png", level: 1, cic: 0 }, } as any; -describe('CreateDropStormParts', () => { - it('renders parts with profile info', async () => { - const parts = [{ content: 'a' }, { content: 'b' }] as any; +describe("CreateDropStormParts", () => { + it("renders parts with profile info", () => { + const parts = [{ content: "a" }, { content: "b" }] as any; render( { /> ); - await waitFor(() => { - expect(screen.getAllByTestId('part-0')[0]).toBeInTheDocument(); - }); - expect(screen.getAllByTestId('part-1')[0]).toBeInTheDocument(); - expect(screen.getByRole('img')).toHaveAttribute('src', 'img.png'); - expect(screen.getByRole('link')).toHaveAttribute('href', '/user'); + expect(screen.getByTestId("part-0")).toBeInTheDocument(); + expect(screen.getByTestId("part-1")).toBeInTheDocument(); + expect(screen.getByRole("img")).toHaveAttribute("src", "img.png"); + expect(screen.getByRole("link")).toHaveAttribute("href", "/user"); }); }); From 5c12de9e0958f82f80adef69608f1e2aa6a4125f Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:03:53 +0200 Subject: [PATCH 11/17] wip Signed-off-by: prxt6529 --- config/sentryProbes.ts | 137 +++++++++++++++++------- instrumentation-client.ts | 215 +++++++++++++++++++++++--------------- 2 files changed, 232 insertions(+), 120 deletions(-) diff --git a/config/sentryProbes.ts b/config/sentryProbes.ts index 377c99555d..c3fdc09614 100644 --- a/config/sentryProbes.ts +++ b/config/sentryProbes.ts @@ -1,3 +1,5 @@ +import type { Event, EventHint } from "@sentry/nextjs"; + const PROBE_PATTERNS = [ ".jsp", ".php", @@ -7,8 +9,8 @@ const PROBE_PATTERNS = [ "/wp-admin", "/wp-login", "/cgi-bin/", - "/manager/html", // Tomcat - "/admin/login.jsp", // common Java probe + "/manager/html", + "/admin/login.jsp", ]; const probeTags = { @@ -16,57 +18,116 @@ const probeTags = { probe_type: "generic-exploit-scan", }; -export function filterTunnelRouteErrors(event: any, hint?: any): any | null { +const CONNECTION_ERROR_PATTERNS = [ + "aborted", + "ECONNRESET", + "socket hang up", +]; + +const HTTP_SERVER_STACK_PATTERNS = [ + "_http_server", + "abortIncoming", + "socketOnClose", +]; + +function isConnectionError(message: string): boolean { + return CONNECTION_ERROR_PATTERNS.some((pattern) => + message.includes(pattern) + ); +} + +function isFrameWithPaths( + frame: unknown +): frame is { filename?: string; abs_path?: string } { + return ( + typeof frame === "object" && + frame !== null && + ("filename" in frame || "abs_path" in frame) + ); +} + +function isMonitoringRoute(url: string, stacktrace: unknown[]): boolean { + if (url.includes("/monitoring")) { + return true; + } + return stacktrace.some((frame) => { + if (!isFrameWithPaths(frame)) { + return false; + } + return ( + frame.filename?.includes("monitoring") || + frame.abs_path?.includes("monitoring") + ); + }); +} + +function hasHttpServerStack(stack: string): boolean { + return HTTP_SERVER_STACK_PATTERNS.some((pattern) => + stack.includes(pattern) + ); +} + +function checkFirstErrorPath( + event: Event, + message: string, + value: { stacktrace?: { frames?: unknown[] } } +): boolean { + if (!isConnectionError(message)) { + return false; + } + + const url = event.request?.url || ""; + const stacktrace = value?.stacktrace?.frames || []; + + return isMonitoringRoute(url, stacktrace); +} + +function checkSecondErrorPath( + event: Event, + message: string, + hint?: EventHint +): boolean { + if (message !== "aborted" || !hint?.originalException) { + return false; + } + + if (!(hint.originalException instanceof Error)) { + return false; + } + + const stack = hint.originalException.stack || ""; + if (!hasHttpServerStack(stack)) { + return false; + } + + const url = event.request?.url || ""; + return url.includes("/monitoring"); +} + +export function filterTunnelRouteErrors( + event: Event, + hint?: EventHint +): Event | null { const value = event.exception?.values?.[0]; const message = value?.value || ""; const errorType = value?.type || ""; if (typeof message === "string") { - if ( - message.includes("aborted") || - message.includes("ECONNRESET") || - message.includes("socket hang up") - ) { - const url = event.request?.url || ""; - const stacktrace = value?.stacktrace?.frames || []; - - const isMonitoringRoute = - url.includes("/monitoring") || - stacktrace.some( - (frame: any) => - frame?.filename?.includes("monitoring") || - frame?.abs_path?.includes("monitoring") - ); - - if (isMonitoringRoute) { - return null; - } + if (checkFirstErrorPath(event, message, value || {})) { + return null; } } if (errorType === "Error" && typeof message === "string") { - if ( - message.includes("aborted") && - hint?.originalException instanceof Error - ) { - const stack = hint.originalException.stack || ""; - if ( - stack.includes("_http_server") || - stack.includes("abortIncoming") || - stack.includes("socketOnClose") - ) { - const url = event.request?.url || ""; - if (url.includes("/monitoring")) { - return null; - } - } + if (checkSecondErrorPath(event, message, hint)) { + return null; } } return event; } -export function tagSecurityProbes(event: any) { +export function tagSecurityProbes(event: Event): Event { try { const url = (event?.request?.url || "").toLowerCase(); diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 450cab1613..d3e423b33d 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -23,6 +23,135 @@ const referenceErrors = ["__firefox__"]; const filenameExceptions = ["inpage.js"]; +const URL_REGEX = /\(([^)]+?)\)/; + +function getFallbackMessage(hint?: Sentry.EventHint): string { + if (typeof hint?.originalException === "string") { + return hint.originalException; + } + if (hint?.originalException instanceof Error) { + return hint.originalException.message; + } + return ""; +} + +function shouldFilterNoisyPatterns(message: string): boolean { + return noisyPatterns.some((p) => message.includes(p)); +} + +function shouldFilterReferenceErrors( + message: string, + errorType: string | undefined +): boolean { + if (errorType !== "ReferenceError" && errorType !== "TypeError") { + return false; + } + return referenceErrors.some((p) => message.includes(p)); +} + +function shouldFilterFilenameExceptions( + frames: Sentry.StackFrame[] | undefined +): boolean { + if (!frames) { + return false; + } + return frames.some((frame) => + filenameExceptions.some( + (pattern) => + frame?.filename?.includes(pattern) || + frame?.abs_path?.includes(pattern) + ) + ); +} + +function shouldFilterEvent( + event: Sentry.Event, + hint?: Sentry.EventHint +): boolean { + const value = event.exception?.values?.[0]; + const fallbackMessage = getFallbackMessage(hint); + const message = value?.value ?? fallbackMessage; + + if (typeof message === "string") { + if (shouldFilterNoisyPatterns(message)) { + return true; + } + if (shouldFilterReferenceErrors(message, value?.type)) { + return true; + } + } + + const frames = event.exception?.values?.[0]?.stacktrace?.frames; + return shouldFilterFilenameExceptions(frames); +} + +function handleIndexedDBError(event: Sentry.Event): void { + event.level = "warning"; + event.tags = { + ...event.tags, + errorType: "indexeddb", + handled: true, + }; + event.fingerprint = ["indexeddb-connection-lost"]; +} + +function extractUrlFromError(error: TypeError, event: Sentry.Event): string { + const urlMatch = URL_REGEX.exec(error.message.slice(0, 2048)); + if (urlMatch?.[1]) { + return urlMatch[1]; + } + + const fetchBreadcrumb = event.breadcrumbs?.find( + (crumb) => crumb.category === "fetch" || crumb.type === "http" + ); + if (fetchBreadcrumb?.data?.url) { + return fetchBreadcrumb.data.url; + } + if (event.request?.url) { + return event.request.url; + } + return "unknown"; +} + +function isNetworkError(errorMessage: string): boolean { + return ( + errorMessage.includes("load failed") || + errorMessage.includes("failed to fetch") || + errorMessage.includes("network") + ); +} + +function handleNetworkError( + event: Sentry.Event, + error: TypeError, + value: Sentry.Exception | undefined +): void { + const errorMessage = error.message.toLowerCase(); + if (!isNetworkError(errorMessage)) { + return; + } + + const url = extractUrlFromError(error, event); + const transformedMessage = errorMessage.includes("network") + ? `Network error: ${error.message} (${url})` + : `Network request failed. Please check your connection and try again. (${url})`; + + if (value) { + value.value = transformedMessage; + } + if (event.message) { + event.message = transformedMessage; + } + + event.level = "warning"; + event.tags = { + ...event.tags, + errorType: "network", + handled: true, + }; + event.fingerprint = ["network-error"]; +} + Sentry.init({ dsn: sentryEnabled ? dsn : undefined, enabled: sentryEnabled, @@ -40,97 +169,19 @@ Sentry.init({ sendDefaultPii: true, beforeSend(event, hint) { - const value = event.exception?.values?.[0]; - - let fallbackMessage = ""; - if (typeof hint?.originalException === "string") { - fallbackMessage = hint.originalException; - } else if (hint?.originalException instanceof Error) { - fallbackMessage = hint.originalException.message; - } - - const message = value?.value ?? fallbackMessage; - const errorType = value?.type; - - if (typeof message === "string") { - if (noisyPatterns.some((p) => message.includes(p))) { - return null; - } - - if ( - (errorType === "ReferenceError" || errorType === "TypeError") && - referenceErrors.some((p) => message.includes(p)) - ) { - return null; - } - } - - const frames = event.exception?.values?.[0]?.stacktrace?.frames; - if ( - frames?.some((frame: any) => - filenameExceptions.some( - (pattern) => - frame?.filename?.includes(pattern) || - frame?.abs_path?.includes(pattern) - ) - ) - ) { + if (shouldFilterEvent(event, hint)) { return null; } const error = hint.originalException || hint.syntheticException; + const value = event.exception?.values?.[0]; if (error && isIndexedDBError(error)) { - event.level = "warning"; - event.tags = { - ...event.tags, - errorType: "indexeddb", - handled: true, - }; - event.fingerprint = ["indexeddb-connection-lost"]; + handleIndexedDBError(event); } if (error instanceof TypeError) { - const errorMessage = error.message.toLowerCase(); - if ( - errorMessage.includes("load failed") || - errorMessage.includes("failed to fetch") || - errorMessage.includes("network") - ) { - let url = "unknown"; - const urlMatch = error.message.slice(0, 2048).match(/\(([^)]+?)\)/); - if (urlMatch) { - url = urlMatch[1]; - } else { - const fetchBreadcrumb = event.breadcrumbs?.find( - (crumb: any) => crumb.category === "fetch" || crumb.type === "http" - ); - if (fetchBreadcrumb?.data?.url) { - url = fetchBreadcrumb.data.url; - } else if (event.request?.url) { - url = event.request.url; - } - } - - const transformedMessage = errorMessage.includes("network") - ? `Network error: ${error.message} (${url})` - : `Network request failed. Please check your connection and try again. (${url})`; - - if (value) { - value.value = transformedMessage; - } - if (event.message) { - event.message = transformedMessage; - } - - event.level = "warning"; - event.tags = { - ...event.tags, - errorType: "network", - handled: true, - }; - event.fingerprint = ["network-error"]; - } + handleNetworkError(event, error, value); } return event; From 9d9c4f02d7011cd0e4fbede95da2264708131add Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:16:55 +0200 Subject: [PATCH 12/17] WIP Signed-off-by: prxt6529 --- instrumentation-client.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/instrumentation-client.ts b/instrumentation-client.ts index d3e423b33d..d41cc15dd6 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -58,8 +58,7 @@ function shouldFilterFilenameExceptions( return frames.some((frame) => filenameExceptions.some( (pattern) => - frame?.filename?.includes(pattern) || - frame?.abs_path?.includes(pattern) + frame?.filename?.includes(pattern) || frame?.abs_path?.includes(pattern) ) ); } @@ -114,10 +113,11 @@ function extractUrlFromError(error: TypeError, event: Sentry.Event): string { } function isNetworkError(errorMessage: string): boolean { + const normalized = errorMessage.toLowerCase(); return ( - errorMessage.includes("load failed") || - errorMessage.includes("failed to fetch") || - errorMessage.includes("network") + normalized.includes("failed to fetch") || + normalized.includes("load failed") || + /\bnetwork\b/.test(normalized) ); } @@ -126,13 +126,13 @@ function handleNetworkError( error: TypeError, value: Sentry.Exception | undefined ): void { - const errorMessage = error.message.toLowerCase(); - if (!isNetworkError(errorMessage)) { + if (!isNetworkError(error.message)) { return; } const url = extractUrlFromError(error, event); - const transformedMessage = errorMessage.includes("network") + const normalized = error.message.toLowerCase(); + const transformedMessage = normalized.includes("network") ? `Network error: ${error.message} (${url})` : `Network request failed. Please check your connection and try again. (${url})`; @@ -173,7 +173,7 @@ Sentry.init({ return null; } - const error = hint.originalException || hint.syntheticException; + const error = hint?.originalException ?? hint?.syntheticException; const value = event.exception?.values?.[0]; if (error && isIndexedDBError(error)) { From 8eaf3af8ce7b4425ebbb9824a3493e54edf81b39 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:19:48 +0200 Subject: [PATCH 13/17] Trigger CI Signed-off-by: prxt6529 From 6cd0418a0efa0b45ecf1d5142f46aff2aa6b9919 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:26:07 +0200 Subject: [PATCH 14/17] Trigger CI Signed-off-by: prxt6529 From ed12adb7b1e4476a7a1eaf0e199b43302636e77b Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:32:29 +0200 Subject: [PATCH 15/17] WIP Signed-off-by: prxt6529 --- config/sentryProbes.ts | 8 ++++---- sentry.edge.config.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/sentryProbes.ts b/config/sentryProbes.ts index c3fdc09614..e108e883a1 100644 --- a/config/sentryProbes.ts +++ b/config/sentryProbes.ts @@ -104,10 +104,10 @@ function checkSecondErrorPath( return url.includes("/monitoring"); } -export function filterTunnelRouteErrors( - event: Event, +export function filterTunnelRouteErrors( + event: T, hint?: EventHint -): Event | null { +): T | null { const value = event.exception?.values?.[0]; const message = value?.value || ""; const errorType = value?.type || ""; @@ -127,7 +127,7 @@ export function filterTunnelRouteErrors( return event; } -export function tagSecurityProbes(event: Event): Event { +export function tagSecurityProbes(event: T): T { try { const url = (event?.request?.url || "").toLowerCase(); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index 6b45c70c7b..247e06d9c8 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -29,7 +29,7 @@ Sentry.init({ // ------------------------------------------------------------ // Handle obvious bot / exploit probes more gently (edge) // ------------------------------------------------------------ - beforeSend(event, hint) { + beforeSend(event: Sentry.ErrorEvent, hint) { const filtered = filterTunnelRouteErrors(event, hint); if (filtered === null) { return null; From 105d999763fe0da23e97ae5b510eb750cf3c7d13 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:42:17 +0200 Subject: [PATCH 16/17] WIP Signed-off-by: prxt6529 --- instrumentation-client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instrumentation-client.ts b/instrumentation-client.ts index d41cc15dd6..cd053329d4 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -117,6 +117,9 @@ function isNetworkError(errorMessage: string): boolean { return ( normalized.includes("failed to fetch") || normalized.includes("load failed") || + normalized.includes("networkerror") || + normalized.includes("network error") || + normalized.includes("network request failed") || /\bnetwork\b/.test(normalized) ); } From 3d83e7268581a2fae38d6b882558a9bad5ae5850 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 3 Dec 2025 16:44:18 +0200 Subject: [PATCH 17/17] WIP Signed-off-by: prxt6529 --- config/sentryProbes.ts | 13 ++++--------- sentry.edge.config.ts | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/config/sentryProbes.ts b/config/sentryProbes.ts index e108e883a1..8ca54ffd6c 100644 --- a/config/sentryProbes.ts +++ b/config/sentryProbes.ts @@ -18,11 +18,7 @@ const probeTags = { probe_type: "generic-exploit-scan", }; -const CONNECTION_ERROR_PATTERNS = [ - "aborted", - "ECONNRESET", - "socket hang up", -]; +const CONNECTION_ERROR_PATTERNS = ["aborted", "ECONNRESET", "socket hang up"]; const HTTP_SERVER_STACK_PATTERNS = [ "_http_server", @@ -31,8 +27,9 @@ const HTTP_SERVER_STACK_PATTERNS = [ ]; function isConnectionError(message: string): boolean { + const normalized = message.toLowerCase(); return CONNECTION_ERROR_PATTERNS.some((pattern) => - message.includes(pattern) + normalized.includes(pattern.toLowerCase()) ); } @@ -62,9 +59,7 @@ function isMonitoringRoute(url: string, stacktrace: unknown[]): boolean { } function hasHttpServerStack(stack: string): boolean { - return HTTP_SERVER_STACK_PATTERNS.some((pattern) => - stack.includes(pattern) - ); + return HTTP_SERVER_STACK_PATTERNS.some((pattern) => stack.includes(pattern)); } function checkFirstErrorPath( diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index 247e06d9c8..9c804c98d9 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -29,7 +29,7 @@ Sentry.init({ // ------------------------------------------------------------ // Handle obvious bot / exploit probes more gently (edge) // ------------------------------------------------------------ - beforeSend(event: Sentry.ErrorEvent, hint) { + beforeSend(event: Sentry.ErrorEvent, hint: Sentry.EventHint) { const filtered = filterTunnelRouteErrors(event, hint); if (filtered === null) { return null;