mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Silently ignore network errors in EventTracker and SessionRecorder flush (#1638)
This commit is contained in:
parent
845487adee
commit
591554c3b1
@ -387,6 +387,39 @@ describe("EventTracker", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("silently ignores network errors caused by ad blockers", async () => {
|
||||
vi.useFakeTimers();
|
||||
document.body.innerHTML = "<button>Click me</button>";
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const sentBodies: string[] = [];
|
||||
const tracker = new EventTracker({
|
||||
projectId: "internal",
|
||||
sendBatch: async (body) => {
|
||||
sentBodies.push(body);
|
||||
return Result.error(new TypeError("Failed to fetch"));
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
tracker.start();
|
||||
|
||||
await advancePastFlush();
|
||||
expect(sentBodies).toHaveLength(1);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
|
||||
// tracker — subsequent flushes continue attempting delivery.
|
||||
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await advancePastFlush();
|
||||
expect(sentBodies).toHaveLength(2);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
tracker.stop();
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
|
||||
vi.useFakeTimers();
|
||||
document.body.innerHTML = "<button>Click me</button>";
|
||||
|
||||
@ -4,7 +4,7 @@ import { cssEscapeIdent } from "@hexclave/shared/dist/utils/dom";
|
||||
import { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from "@hexclave/shared/dist/utils/elements-chain";
|
||||
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
||||
import { Result } from "@hexclave/shared/dist/utils/results";
|
||||
import { generateUuid, isAnalyticsNotEnabledError } from "./session-replay";
|
||||
import { generateUuid, isAdBlockerNetworkError, isAnalyticsNotEnabledError } from "./session-replay";
|
||||
|
||||
const FLUSH_INTERVAL_MS = 10_000;
|
||||
const MAX_EVENTS_PER_BATCH = 50;
|
||||
@ -507,6 +507,11 @@ export class EventTracker {
|
||||
this._disable();
|
||||
return;
|
||||
}
|
||||
// Ad blockers commonly block analytics endpoints, causing network
|
||||
// errors. These are expected and should not pollute the console.
|
||||
if (isAdBlockerNetworkError(res.error)) {
|
||||
return;
|
||||
}
|
||||
console.warn("EventTracker flush failed:", res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -45,6 +45,58 @@ describe("analytics option JSON conversion", () => {
|
||||
});
|
||||
|
||||
describe("SessionRecorder flush", () => {
|
||||
it("silently ignores network errors caused by ad blockers", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const storageKey = `hexclave:session-replay:v1:test-project`;
|
||||
localStorage.setItem(storageKey, JSON.stringify({
|
||||
session_id: "test-session",
|
||||
created_at_ms: Date.now(),
|
||||
last_activity_ms: Date.now(),
|
||||
}));
|
||||
|
||||
const sentBodies: string[] = [];
|
||||
const recorder = new SessionRecorder(
|
||||
{
|
||||
projectId: "test-project",
|
||||
sendBatch: async (body) => {
|
||||
sentBodies.push(body);
|
||||
return Result.error(new TypeError("Failed to fetch"));
|
||||
},
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
(recorder as any)._tick();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(sentBodies).toHaveLength(1);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Unlike ANALYTICS_NOT_ENABLED, ad blocker errors do NOT disable the
|
||||
// recorder — subsequent flushes continue attempting delivery.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
(recorder as any)._tick();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(sentBodies).toHaveLength(2);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
recorder.stop();
|
||||
warnSpy.mockRestore();
|
||||
localStorage.removeItem(storageKey);
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
|
||||
@ -166,6 +166,21 @@ export function isAnalyticsNotEnabledError(error: unknown): boolean {
|
||||
return KnownErrors.AnalyticsNotEnabled.isInstance(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the error looks like a network failure caused by an ad blocker or
|
||||
* similar extension blocking analytics requests. These are expected in
|
||||
* production and should be silently ignored rather than logged as warnings.
|
||||
*/
|
||||
export function isAdBlockerNetworkError(error: unknown): boolean {
|
||||
if (error instanceof Error) {
|
||||
return error.message.includes("Failed to fetch")
|
||||
|| error.message.includes("NetworkError")
|
||||
|| error.message.includes("Load failed")
|
||||
|| error.message.includes("network connection");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export class SessionRecorder {
|
||||
private _started = false;
|
||||
private _cancelled = false;
|
||||
@ -274,6 +289,11 @@ export class SessionRecorder {
|
||||
this._disable();
|
||||
return;
|
||||
}
|
||||
// Ad blockers commonly block analytics endpoints, causing network
|
||||
// errors. These are expected and should not pollute the console.
|
||||
if (isAdBlockerNetworkError(res.error)) {
|
||||
return;
|
||||
}
|
||||
captureWarning("SessionRecorder.flush", res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user