cloudflare-next-intl 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -5
- package/dist/src/client/components/client_provider.js +2 -0
- package/dist/src/cookie_consent/gdpr_countries.d.ts +2 -2
- package/dist/src/cookie_consent/gdpr_countries.js +15 -4
- package/dist/src/error_handling/default_ignored_console_errors.d.ts +13 -0
- package/dist/src/error_handling/default_ignored_console_errors.js +27 -0
- package/dist/src/error_handling/format_error_message.d.ts +9 -0
- package/dist/src/error_handling/format_error_message.js +23 -0
- package/dist/src/error_handling/index.d.ts +9 -0
- package/dist/src/error_handling/index.js +6 -0
- package/dist/src/error_handling/install_console_error_override.bench.d.ts +1 -0
- package/dist/src/error_handling/install_console_error_override.bench.js +10 -0
- package/dist/src/error_handling/install_console_error_override.d.ts +30 -0
- package/dist/src/error_handling/install_console_error_override.js +55 -0
- package/dist/src/error_handling/report_error.bench.d.ts +1 -0
- package/dist/src/error_handling/report_error.bench.js +25 -0
- package/dist/src/error_handling/report_error.d.ts +38 -0
- package/dist/src/error_handling/report_error.js +60 -0
- package/dist/src/error_handling/stringify_unknown.bench.d.ts +1 -0
- package/dist/src/error_handling/stringify_unknown.bench.js +29 -0
- package/dist/src/error_handling/stringify_unknown.d.ts +12 -0
- package/dist/src/error_handling/stringify_unknown.js +42 -0
- package/dist/src/error_handling/with_error_handling.d.ts +15 -0
- package/dist/src/error_handling/with_error_handling.js +18 -0
- package/dist/src/firebase_auth/server/firebase_server.js +3 -1
- package/dist/src/server/components/server_provider.js +15 -4
- package/dist/src/types/types.d.ts +116 -25
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ and Cloudflare environment.
|
|
|
11
11
|
Components.
|
|
12
12
|
- **Fast and Efficient**: Low overhead and minimal bundle size.
|
|
13
13
|
- **Tree-shaking**: Properly architected for optimal tree-shaking.
|
|
14
|
+
- **Error handling**: shared, opt-in `console.error` override and
|
|
15
|
+
`reportError`/`withErrorHandling` helpers, GDPR-aware (consent-gated).
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -200,16 +202,21 @@ import { getCloudflareContext } from "@opennextjs/cloudflare";
|
|
|
200
202
|
export default setIntlConfig({
|
|
201
203
|
locales: ["en", "de"],
|
|
202
204
|
defaultLocale: "en",
|
|
205
|
+
// Shared request-time resolvers — used by cookieConsent's GDPR gating
|
|
206
|
+
// below AND by error_handling's ctx.waitUntil backgrounding.
|
|
207
|
+
generate: {
|
|
208
|
+
// Pass @opennextjs/cloudflare's getCloudflareContext directly — its
|
|
209
|
+
// exact overloaded signature is accepted as-is, called internally
|
|
210
|
+
// with { async: true } (cookieConsent) or { async: false } (error_handling).
|
|
211
|
+
getCloudflareContext,
|
|
212
|
+
},
|
|
203
213
|
cookieConsent: {
|
|
204
214
|
privacyPolicyDate: "2026-01-01",
|
|
205
215
|
// privacyPolicyPath: "/privacy-policy", // default; used by the
|
|
206
216
|
// dialogs' auto-rendered link. Set false to disable that link.
|
|
207
217
|
// Optional: gate the banner to GDPR-region visitors only. Omit both
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// exact overloaded signature is accepted as-is, called internally
|
|
211
|
-
// with { async: true }.
|
|
212
|
-
getCloudflareContext,
|
|
218
|
+
// getCountryCode and generate.getCloudflareContext to disable
|
|
219
|
+
// country-based gating (consent always implicit).
|
|
213
220
|
// gdprCountries: [...], // defaults to EU/EEA + UK + Switzerland
|
|
214
221
|
// enableAnalyticsInDevMode: true, // analytics stay off in dev otherwise
|
|
215
222
|
// autoWireDialogs: false, // opt out and render the dialogs yourself
|
|
@@ -246,6 +253,58 @@ const { consent, setConsent } = useCookieConsent();
|
|
|
246
253
|
|
|
247
254
|
See [`package/src/cookie_consent/README.md`](package/src/cookie_consent/README.md) for layout, customization, and gotchas.
|
|
248
255
|
|
|
256
|
+
### Error handling
|
|
257
|
+
|
|
258
|
+
Every risky call this package makes internally (Cloudflare-context
|
|
259
|
+
resolution, Firebase server auth, `cookieConsent.getAnalytics()`) reports
|
|
260
|
+
through a shared `error_handling` submodule you can also use in your own
|
|
261
|
+
app code. Enabled by default — no config needed to get the default
|
|
262
|
+
`console.error`-based reporting; set `errorHandling.onError` to plug in
|
|
263
|
+
your own transport (Sentry, Telegram, etc).
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// intl-config.ts
|
|
267
|
+
export default setIntlConfig({
|
|
268
|
+
locales: ["en", "de"],
|
|
269
|
+
defaultLocale: "en",
|
|
270
|
+
generate: { getCloudflareContext }, // reports background via ctx.waitUntil when set
|
|
271
|
+
errorHandling: {
|
|
272
|
+
// enable: false, // fully disable reporting (errors still rethrow from withErrorHandling)
|
|
273
|
+
onError: ({ formattedMessage, error, classOrMethodName, consent }) => {
|
|
274
|
+
// formattedMessage is a ready-to-print "[classOrMethodName] Error: ..." string
|
|
275
|
+
myErrorTracker.capture(formattedMessage);
|
|
276
|
+
},
|
|
277
|
+
// overrideConsoleError: true, // route every console.error(...) call through onError too
|
|
278
|
+
// ignoreConsoleErrors: [...], // defaults to defaultIgnoredConsoleErrors (this package's
|
|
279
|
+
// // own Firebase Auth codes for expected user-input failures);
|
|
280
|
+
// // pass [] to report everything, or your own list to replace it
|
|
281
|
+
// ignoreConsoleError: (message) => message.includes("known noisy warning"),
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Use `reportError`/`withErrorHandling` directly in your own code:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { reportError, withErrorHandling } from "cloudflare-next-intl/errorHandling";
|
|
290
|
+
import config from "./intl-config";
|
|
291
|
+
|
|
292
|
+
// Wrap a function — reports then rethrows on failure.
|
|
293
|
+
const safeFetch = withErrorHandling(fetchSomething, "fetchSomething", { config });
|
|
294
|
+
|
|
295
|
+
// Or report manually inside your own try/catch.
|
|
296
|
+
try {
|
|
297
|
+
await riskyThing();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
await reportError(config, { error, classOrMethodName: "riskyThing" });
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**GDPR note:** pass `consent` (from `useCookieConsent()` or your own
|
|
304
|
+
server-side resolution) on `ErrorHandlingParams` — reporting is skipped
|
|
305
|
+
whenever `consent` is set and not `true`, since sending error reports to a
|
|
306
|
+
third party without consent can itself be GDPR-relevant.
|
|
307
|
+
|
|
249
308
|
## License
|
|
250
309
|
|
|
251
310
|
MIT
|
|
@@ -4,6 +4,7 @@ import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_va
|
|
|
4
4
|
import { createContext, useMemo } from "react";
|
|
5
5
|
import dynamic from "next/dynamic";
|
|
6
6
|
import config from "@intl-config";
|
|
7
|
+
import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
|
|
7
8
|
export const LocaleContext = createContext(undefined);
|
|
8
9
|
// Hoisted to module scope — calling `dynamic()` inside the component body
|
|
9
10
|
// creates a brand-new component identity every render, forcing React to
|
|
@@ -20,6 +21,7 @@ const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/cli
|
|
|
20
21
|
export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
21
22
|
setLocaleCache(language);
|
|
22
23
|
setMessageForLocaleCache(language, messages);
|
|
24
|
+
installConsoleErrorOverride(config, true);
|
|
23
25
|
// `LocaleContext.Provider` stays the outermost element here — the
|
|
24
26
|
// client `AuthUserProvider` (and its descendants calling
|
|
25
27
|
// usePathname()/useLocale()) must render as a CHILD of it, not a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CookieConsentGetCloudflareContext } from '../types/types';
|
|
1
|
+
import type { CookieConsentGetCloudflareContext, ErrorHandlingRoutingConfig } from '../types/types';
|
|
2
2
|
/**
|
|
3
3
|
* Default `cookieConsent.gdprCountries` — EU/EEA member states (GDPR),
|
|
4
4
|
* Iceland/Liechtenstein/Norway (EEA), the UK (UK-GDPR), and Switzerland
|
|
@@ -15,4 +15,4 @@ export declare const defaultGdprCountries: readonly string[];
|
|
|
15
15
|
* `gdprCountries` skips the banner. `getCountryCode` takes precedence
|
|
16
16
|
* over `getCloudflareContext` when both are set.
|
|
17
17
|
*/
|
|
18
|
-
export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined): Promise<boolean>;
|
|
18
|
+
export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined, errorHandlingConfig?: ErrorHandlingRoutingConfig): Promise<boolean>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import reportError from '../error_handling/report_error';
|
|
1
2
|
/**
|
|
2
3
|
* Default `cookieConsent.gdprCountries` — EU/EEA member states (GDPR),
|
|
3
4
|
* Iceland/Liechtenstein/Norway (EEA), the UK (UK-GDPR), and Switzerland
|
|
@@ -40,12 +41,22 @@ function getGdprCountriesSet(gdprCountries) {
|
|
|
40
41
|
* `gdprCountries` skips the banner. `getCountryCode` takes precedence
|
|
41
42
|
* over `getCloudflareContext` when both are set.
|
|
42
43
|
*/
|
|
43
|
-
export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries) {
|
|
44
|
+
export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries, errorHandlingConfig) {
|
|
44
45
|
if (!getCountryCode && !getCloudflareContext)
|
|
45
46
|
return true;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
let countryCode;
|
|
48
|
+
if (getCountryCode) {
|
|
49
|
+
countryCode = await getCountryCode();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
countryCode = (await getCloudflareContext({ async: true }))?.cf?.country;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
await reportError({ errorHandling: errorHandlingConfig, generate: { getCloudflareContext } }, { error, classOrMethodName: 'resolveRequiresConsent' });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
49
60
|
if (typeof countryCode !== 'string' || !countryCode)
|
|
50
61
|
return true;
|
|
51
62
|
return getGdprCountriesSet(gdprCountries).has(countryCode);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Auth error codes this package's own `createLoginAction`/
|
|
3
|
+
* `createSignUpAction`/`createForgotPasswordAction` already catch and
|
|
4
|
+
* translate into a localized message (see
|
|
5
|
+
* `firebase_auth/error_messages/firebase_auth_error_helper.ts`) — expected
|
|
6
|
+
* user-input failures (wrong password, email already in use, etc.), not
|
|
7
|
+
* bugs. They never reach `console.error`/`reportError` through this
|
|
8
|
+
* package's own code; this list exists as defense-in-depth for consumers
|
|
9
|
+
* whose own code logs one of these codes directly. Passing your own
|
|
10
|
+
* `ignoreConsoleError` array replaces this default entirely — pass `[]` to
|
|
11
|
+
* report everything.
|
|
12
|
+
*/
|
|
13
|
+
export declare const defaultIgnoredConsoleErrors: readonly string[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Auth error codes this package's own `createLoginAction`/
|
|
3
|
+
* `createSignUpAction`/`createForgotPasswordAction` already catch and
|
|
4
|
+
* translate into a localized message (see
|
|
5
|
+
* `firebase_auth/error_messages/firebase_auth_error_helper.ts`) — expected
|
|
6
|
+
* user-input failures (wrong password, email already in use, etc.), not
|
|
7
|
+
* bugs. They never reach `console.error`/`reportError` through this
|
|
8
|
+
* package's own code; this list exists as defense-in-depth for consumers
|
|
9
|
+
* whose own code logs one of these codes directly. Passing your own
|
|
10
|
+
* `ignoreConsoleError` array replaces this default entirely — pass `[]` to
|
|
11
|
+
* report everything.
|
|
12
|
+
*/
|
|
13
|
+
export const defaultIgnoredConsoleErrors = [
|
|
14
|
+
'auth/invalid-email',
|
|
15
|
+
'auth/user-disabled',
|
|
16
|
+
'auth/user-not-found',
|
|
17
|
+
'auth/wrong-password',
|
|
18
|
+
'auth/invalid-credential',
|
|
19
|
+
'auth/email-already-in-use',
|
|
20
|
+
'auth/weak-password',
|
|
21
|
+
'auth/too-many-requests',
|
|
22
|
+
'auth/network-request-failed',
|
|
23
|
+
'auth/requires-recent-login',
|
|
24
|
+
'auth/expired-action-code',
|
|
25
|
+
'auth/invalid-action-code',
|
|
26
|
+
'auth/user-token-expired',
|
|
27
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ErrorHandlingParams } from '../types/types';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a human-readable one-string summary of an `ErrorHandlingParams` —
|
|
4
|
+
* `[classOrMethodName] Error: <message>` followed by non-empty `Params`/
|
|
5
|
+
* `IsClient` sections. Never throws (`stringifyUnknown` is safe). Used as
|
|
6
|
+
* `ErrorHandlingParams.formattedMessage` — read this instead of `error`/
|
|
7
|
+
* `params` directly when you just want something printable.
|
|
8
|
+
*/
|
|
9
|
+
export default function formatErrorMessage(params: ErrorHandlingParams): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import stringifyUnknown from './stringify_unknown';
|
|
2
|
+
function formatSection(title, value, isClient) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return '';
|
|
5
|
+
const text = stringifyUnknown(value, isClient, true);
|
|
6
|
+
if (!text || text === '{}' || text === '[]')
|
|
7
|
+
return '';
|
|
8
|
+
return `\n${title}: ${text}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Builds a human-readable one-string summary of an `ErrorHandlingParams` —
|
|
12
|
+
* `[classOrMethodName] Error: <message>` followed by non-empty `Params`/
|
|
13
|
+
* `IsClient` sections. Never throws (`stringifyUnknown` is safe). Used as
|
|
14
|
+
* `ErrorHandlingParams.formattedMessage` — read this instead of `error`/
|
|
15
|
+
* `params` directly when you just want something printable.
|
|
16
|
+
*/
|
|
17
|
+
export default function formatErrorMessage(params) {
|
|
18
|
+
const { error, classOrMethodName, params: extraParams, isClient } = params;
|
|
19
|
+
const errorText = stringifyUnknown(error, isClient);
|
|
20
|
+
const paramsSection = formatSection('Params', extraParams, isClient);
|
|
21
|
+
const clientSection = isClient ? '\nSource: client' : '';
|
|
22
|
+
return `[${classOrMethodName}] Error: ${errorText}${paramsSection}${clientSection}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as withErrorHandling } from './with_error_handling';
|
|
2
|
+
export type { WithErrorHandlingOptions } from './with_error_handling';
|
|
3
|
+
export { default as reportError } from './report_error';
|
|
4
|
+
export type { ReportErrorConfig } from './report_error';
|
|
5
|
+
export { default as installConsoleErrorOverride } from './install_console_error_override';
|
|
6
|
+
export { default as stringifyUnknown } from './stringify_unknown';
|
|
7
|
+
export { default as formatErrorMessage } from './format_error_message';
|
|
8
|
+
export { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
|
|
9
|
+
export type { ErrorHandlingParams, ErrorHandlingRoutingConfig } from '../types/types';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as withErrorHandling } from './with_error_handling';
|
|
2
|
+
export { default as reportError } from './report_error';
|
|
3
|
+
export { default as installConsoleErrorOverride } from './install_console_error_override';
|
|
4
|
+
export { default as stringifyUnknown } from './stringify_unknown';
|
|
5
|
+
export { default as formatErrorMessage } from './format_error_message';
|
|
6
|
+
export { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import installConsoleErrorOverride from './install_console_error_override';
|
|
3
|
+
describe('installConsoleErrorOverride: repeated install calls (no-op after first)', () => {
|
|
4
|
+
const originalConsoleError = console.error;
|
|
5
|
+
console.error = () => { };
|
|
6
|
+
bench('install() called repeatedly (marker check short-circuits)', () => {
|
|
7
|
+
installConsoleErrorOverride({ errorHandling: { overrideConsoleError: true } });
|
|
8
|
+
});
|
|
9
|
+
console.error = originalConsoleError;
|
|
10
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReportErrorConfig } from './report_error';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces the global `console.error` so every `console.error(...)` call is
|
|
4
|
+
* also routed through `config.errorHandling.onError`/`reportError` — the
|
|
5
|
+
* original `console.error` still runs afterwards, nothing is swallowed.
|
|
6
|
+
* Safe to call more than once (a no-op after the first call in this JS
|
|
7
|
+
* realm — call it separately on the server and on the client, each has its
|
|
8
|
+
* own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
|
|
9
|
+
* is `true`.
|
|
10
|
+
*
|
|
11
|
+
* Caps at `MAX_REPORTS_PER_INSTALL` (20) reports per install — a component
|
|
12
|
+
* stuck in a render-error loop calls `console.error` on every render, and
|
|
13
|
+
* without a cap this would report (and, server-side, background via
|
|
14
|
+
* `waitUntil`) unboundedly. Once the cap is hit, `console.error` still runs
|
|
15
|
+
* normally, it just stops being reported.
|
|
16
|
+
*
|
|
17
|
+
* `config.errorHandling.ignoreConsoleErrors` (default
|
|
18
|
+
* `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
|
|
19
|
+
* codes for expected user-input failures) and `ignoreConsoleError` both
|
|
20
|
+
* skip reporting a matching call while still logging it normally.
|
|
21
|
+
*
|
|
22
|
+
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
23
|
+
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
24
|
+
* @param isClient Passed through to every report's `ErrorHandlingParams.isClient`
|
|
25
|
+
* — set `true` when installing from client-side code (e.g. the client
|
|
26
|
+
* `LocationzationClientProvider`), omit/`false` on the server. There's no
|
|
27
|
+
* `getCloudflareContext`/`ctx.waitUntil` available in the browser, so
|
|
28
|
+
* client-side reports always await `onError` directly.
|
|
29
|
+
*/
|
|
30
|
+
export default function installConsoleErrorOverride(config: ReportErrorConfig | undefined, isClient?: boolean): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import reportError from './report_error';
|
|
2
|
+
import stringifyUnknown from './stringify_unknown';
|
|
3
|
+
import { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
|
|
4
|
+
const MAX_REPORTS_PER_INSTALL = 20;
|
|
5
|
+
/**
|
|
6
|
+
* Replaces the global `console.error` so every `console.error(...)` call is
|
|
7
|
+
* also routed through `config.errorHandling.onError`/`reportError` — the
|
|
8
|
+
* original `console.error` still runs afterwards, nothing is swallowed.
|
|
9
|
+
* Safe to call more than once (a no-op after the first call in this JS
|
|
10
|
+
* realm — call it separately on the server and on the client, each has its
|
|
11
|
+
* own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
|
|
12
|
+
* is `true`.
|
|
13
|
+
*
|
|
14
|
+
* Caps at `MAX_REPORTS_PER_INSTALL` (20) reports per install — a component
|
|
15
|
+
* stuck in a render-error loop calls `console.error` on every render, and
|
|
16
|
+
* without a cap this would report (and, server-side, background via
|
|
17
|
+
* `waitUntil`) unboundedly. Once the cap is hit, `console.error` still runs
|
|
18
|
+
* normally, it just stops being reported.
|
|
19
|
+
*
|
|
20
|
+
* `config.errorHandling.ignoreConsoleErrors` (default
|
|
21
|
+
* `defaultIgnoredConsoleErrors` — this package's own Firebase Auth error
|
|
22
|
+
* codes for expected user-input failures) and `ignoreConsoleError` both
|
|
23
|
+
* skip reporting a matching call while still logging it normally.
|
|
24
|
+
*
|
|
25
|
+
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
26
|
+
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
27
|
+
* @param isClient Passed through to every report's `ErrorHandlingParams.isClient`
|
|
28
|
+
* — set `true` when installing from client-side code (e.g. the client
|
|
29
|
+
* `LocationzationClientProvider`), omit/`false` on the server. There's no
|
|
30
|
+
* `getCloudflareContext`/`ctx.waitUntil` available in the browser, so
|
|
31
|
+
* client-side reports always await `onError` directly.
|
|
32
|
+
*/
|
|
33
|
+
export default function installConsoleErrorOverride(config, isClient) {
|
|
34
|
+
if (config?.errorHandling?.overrideConsoleError !== true)
|
|
35
|
+
return;
|
|
36
|
+
if (console.error.__isErrorHandlingOverride)
|
|
37
|
+
return;
|
|
38
|
+
const originalConsoleError = console.error.bind(console);
|
|
39
|
+
let reportCount = 0;
|
|
40
|
+
const override = (message, ...optionalParams) => {
|
|
41
|
+
originalConsoleError(message, ...optionalParams);
|
|
42
|
+
if (reportCount >= MAX_REPORTS_PER_INSTALL)
|
|
43
|
+
return;
|
|
44
|
+
const stringified = stringifyUnknown(message, isClient);
|
|
45
|
+
const ignoreList = config.errorHandling?.ignoreConsoleErrors ?? defaultIgnoredConsoleErrors;
|
|
46
|
+
if (ignoreList.some((ignored) => stringified.includes(ignored)))
|
|
47
|
+
return;
|
|
48
|
+
if (config.errorHandling?.ignoreConsoleError?.(stringified))
|
|
49
|
+
return;
|
|
50
|
+
reportCount++;
|
|
51
|
+
void reportError(config, { error: message, classOrMethodName: 'Global Console Error Handler', params: optionalParams, isClient });
|
|
52
|
+
};
|
|
53
|
+
override.__isErrorHandlingOverride = true;
|
|
54
|
+
console.error = override;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import reportError from './report_error';
|
|
3
|
+
const noopOnError = () => { };
|
|
4
|
+
const waitUntil = () => { };
|
|
5
|
+
const fakeGetCloudflareContext = (() => ({ ctx: { waitUntil } }));
|
|
6
|
+
describe('reportError', () => {
|
|
7
|
+
bench('default console.error path (no config)', async () => {
|
|
8
|
+
const original = console.error;
|
|
9
|
+
console.error = noopOnError;
|
|
10
|
+
await reportError(undefined, { error: new Error('boom'), classOrMethodName: 'bench' });
|
|
11
|
+
console.error = original;
|
|
12
|
+
});
|
|
13
|
+
bench('custom onError, no getCloudflareContext (awaited inline)', async () => {
|
|
14
|
+
await reportError({ errorHandling: { onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench' });
|
|
15
|
+
});
|
|
16
|
+
bench('backgrounded via ctx.waitUntil', async () => {
|
|
17
|
+
await reportError({ errorHandling: { onError: noopOnError }, generate: { getCloudflareContext: fakeGetCloudflareContext } }, { error: new Error('boom'), classOrMethodName: 'bench' });
|
|
18
|
+
});
|
|
19
|
+
bench('skipped entirely (enable: false, no formatting cost paid)', async () => {
|
|
20
|
+
await reportError({ errorHandling: { enable: false, onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench' });
|
|
21
|
+
});
|
|
22
|
+
bench('skipped entirely (consent: false)', async () => {
|
|
23
|
+
await reportError({ errorHandling: { onError: noopOnError } }, { error: new Error('boom'), classOrMethodName: 'bench', consent: false });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ErrorHandlingParams, ErrorHandlingRoutingConfig, GenerateRoutingConfig } from '../types/types';
|
|
2
|
+
export interface ReportErrorConfig {
|
|
3
|
+
errorHandling?: ErrorHandlingRoutingConfig;
|
|
4
|
+
generate?: GenerateRoutingConfig;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reports `params` via `config.errorHandling.onError` (default
|
|
8
|
+
* `console.error(params.formattedMessage)`), unless
|
|
9
|
+
* `config.errorHandling.enable === false` or `params.consent` is set and
|
|
10
|
+
* not `true` (reporting to a third party without cookie consent can itself
|
|
11
|
+
* be GDPR-relevant). Never throws — a broken reporter must not mask the
|
|
12
|
+
* original error.
|
|
13
|
+
*
|
|
14
|
+
* Always overwrites `params.formattedMessage` with a fresh
|
|
15
|
+
* `formatErrorMessage(params)` before reporting — a human-readable one-line
|
|
16
|
+
* summary (`[classOrMethodName] Error: <message>` plus non-empty sections)
|
|
17
|
+
* instead of the raw `error`/`params` object, for a default reporter (or a
|
|
18
|
+
* simple `onError`) to print directly.
|
|
19
|
+
*
|
|
20
|
+
* No built-in dedup/throttling: this package has no per-request context to
|
|
21
|
+
* safely scope such state to (module-scope state would leak across
|
|
22
|
+
* concurrent requests in a long-lived server process). Do dedup/throttling
|
|
23
|
+
* in your own `onError` if you need it, scoped to your own request context.
|
|
24
|
+
*
|
|
25
|
+
* When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
|
|
26
|
+
* SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
|
|
27
|
+
* Cloudflare Workers only extends the request's lifetime for work already
|
|
28
|
+
* registered with `waitUntil` by the time the handler returns; deferring
|
|
29
|
+
* that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
|
|
30
|
+
* risks the isolate tearing down the request before `waitUntil` is ever
|
|
31
|
+
* actually invoked, silently dropping the report. Falls back to awaiting
|
|
32
|
+
* `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
|
|
33
|
+
* unavailable (e.g. outside a Cloudflare Worker).
|
|
34
|
+
*
|
|
35
|
+
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
36
|
+
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
37
|
+
*/
|
|
38
|
+
export default function reportError(config: ReportErrorConfig | undefined, params: ErrorHandlingParams): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import formatErrorMessage from './format_error_message';
|
|
2
|
+
async function callOnError(config, params) {
|
|
3
|
+
const paramsWithFormattedMessage = { ...params, formattedMessage: formatErrorMessage(params) };
|
|
4
|
+
try {
|
|
5
|
+
if (config?.onError) {
|
|
6
|
+
await config.onError(paramsWithFormattedMessage);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
console.error(paramsWithFormattedMessage.formattedMessage);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
console.error(paramsWithFormattedMessage.formattedMessage);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reports `params` via `config.errorHandling.onError` (default
|
|
18
|
+
* `console.error(params.formattedMessage)`), unless
|
|
19
|
+
* `config.errorHandling.enable === false` or `params.consent` is set and
|
|
20
|
+
* not `true` (reporting to a third party without cookie consent can itself
|
|
21
|
+
* be GDPR-relevant). Never throws — a broken reporter must not mask the
|
|
22
|
+
* original error.
|
|
23
|
+
*
|
|
24
|
+
* Always overwrites `params.formattedMessage` with a fresh
|
|
25
|
+
* `formatErrorMessage(params)` before reporting — a human-readable one-line
|
|
26
|
+
* summary (`[classOrMethodName] Error: <message>` plus non-empty sections)
|
|
27
|
+
* instead of the raw `error`/`params` object, for a default reporter (or a
|
|
28
|
+
* simple `onError`) to print directly.
|
|
29
|
+
*
|
|
30
|
+
* No built-in dedup/throttling: this package has no per-request context to
|
|
31
|
+
* safely scope such state to (module-scope state would leak across
|
|
32
|
+
* concurrent requests in a long-lived server process). Do dedup/throttling
|
|
33
|
+
* in your own `onError` if you need it, scoped to your own request context.
|
|
34
|
+
*
|
|
35
|
+
* When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
|
|
36
|
+
* SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
|
|
37
|
+
* Cloudflare Workers only extends the request's lifetime for work already
|
|
38
|
+
* registered with `waitUntil` by the time the handler returns; deferring
|
|
39
|
+
* that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
|
|
40
|
+
* risks the isolate tearing down the request before `waitUntil` is ever
|
|
41
|
+
* actually invoked, silently dropping the report. Falls back to awaiting
|
|
42
|
+
* `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
|
|
43
|
+
* unavailable (e.g. outside a Cloudflare Worker).
|
|
44
|
+
*
|
|
45
|
+
* @param config Pass the relevant slices of your `RoutingConfig` directly —
|
|
46
|
+
* `{ errorHandling: config.errorHandling, generate: config.generate }`.
|
|
47
|
+
*/
|
|
48
|
+
export default async function reportError(config, params) {
|
|
49
|
+
const errorHandling = config?.errorHandling;
|
|
50
|
+
if (errorHandling?.enable === false)
|
|
51
|
+
return;
|
|
52
|
+
if (params.consent !== undefined && params.consent !== true)
|
|
53
|
+
return;
|
|
54
|
+
const waitUntil = config?.generate?.getCloudflareContext?.({ async: false })?.ctx?.waitUntil;
|
|
55
|
+
if (waitUntil) {
|
|
56
|
+
waitUntil(callOnError(errorHandling, params));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await callOnError(errorHandling, params);
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import stringifyUnknown from './stringify_unknown';
|
|
3
|
+
const error = new Error('boom');
|
|
4
|
+
const plainObject = { a: 1, b: { c: 2, d: [1, 2, 3] } };
|
|
5
|
+
const circular = {};
|
|
6
|
+
circular.self = circular;
|
|
7
|
+
describe('stringifyUnknown', () => {
|
|
8
|
+
bench('string passthrough', () => {
|
|
9
|
+
stringifyUnknown('already a string');
|
|
10
|
+
});
|
|
11
|
+
bench('Error instance', () => {
|
|
12
|
+
stringifyUnknown(error);
|
|
13
|
+
});
|
|
14
|
+
bench('function-wrapped lazy error (server, resolved)', () => {
|
|
15
|
+
stringifyUnknown(() => 'lazy boom');
|
|
16
|
+
});
|
|
17
|
+
bench('function-wrapped lazy error (client, not resolved)', () => {
|
|
18
|
+
stringifyUnknown(() => 'lazy boom', true);
|
|
19
|
+
});
|
|
20
|
+
bench('plain object, pretty-printed', () => {
|
|
21
|
+
stringifyUnknown(plainObject);
|
|
22
|
+
});
|
|
23
|
+
bench('plain object, nested (compact)', () => {
|
|
24
|
+
stringifyUnknown(plainObject, false, true);
|
|
25
|
+
});
|
|
26
|
+
bench('circular object (falls back to [Unserializable value])', () => {
|
|
27
|
+
stringifyUnknown(circular, false, true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely converts an `unknown` value (typically `ErrorHandlingParams.error`)
|
|
3
|
+
* into a string, for logging/dedup-keying/display — never throws.
|
|
4
|
+
*
|
|
5
|
+
* @param isClient Skips resolving function-wrapped/lazy error values on the
|
|
6
|
+
* client (matches `ErrorHandlingParams.isClient`) — running arbitrary
|
|
7
|
+
* caught functions client-side isn't safe the way it is on the server.
|
|
8
|
+
* @param isNested Set when stringifying a value nested inside another
|
|
9
|
+
* object/array — falls back to a plain `JSON.stringify` (no pretty-print)
|
|
10
|
+
* so a single unserializable nested value can't crash the whole report.
|
|
11
|
+
*/
|
|
12
|
+
export default function stringifyUnknown(value: unknown, isClient?: boolean, isNested?: boolean): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const MAX_FUNCTION_RESOLUTION_ATTEMPTS = 5;
|
|
2
|
+
function resolveFunctionError(value) {
|
|
3
|
+
let result = value;
|
|
4
|
+
try {
|
|
5
|
+
for (let i = 0; i < MAX_FUNCTION_RESOLUTION_ATTEMPTS && typeof result === 'function'; i++) {
|
|
6
|
+
result = result();
|
|
7
|
+
}
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return `Error during function resolution: ${String(error)}`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Safely converts an `unknown` value (typically `ErrorHandlingParams.error`)
|
|
16
|
+
* into a string, for logging/dedup-keying/display — never throws.
|
|
17
|
+
*
|
|
18
|
+
* @param isClient Skips resolving function-wrapped/lazy error values on the
|
|
19
|
+
* client (matches `ErrorHandlingParams.isClient`) — running arbitrary
|
|
20
|
+
* caught functions client-side isn't safe the way it is on the server.
|
|
21
|
+
* @param isNested Set when stringifying a value nested inside another
|
|
22
|
+
* object/array — falls back to a plain `JSON.stringify` (no pretty-print)
|
|
23
|
+
* so a single unserializable nested value can't crash the whole report.
|
|
24
|
+
*/
|
|
25
|
+
export default function stringifyUnknown(value, isClient, isNested = false) {
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return value;
|
|
28
|
+
if (value instanceof Error)
|
|
29
|
+
return `${value.name}: ${value.message}\n\n${value.stack ?? ''}`;
|
|
30
|
+
if (typeof value === 'function') {
|
|
31
|
+
if (isClient)
|
|
32
|
+
return '[Function]';
|
|
33
|
+
const resolved = resolveFunctionError(value);
|
|
34
|
+
return typeof resolved !== 'function' ? stringifyUnknown(resolved, isClient) : '[Function]';
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return isNested ? JSON.stringify(value) : JSON.stringify(value, null, 2);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return '[Unserializable value]';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConsentValue } from '../cookie_consent/types';
|
|
2
|
+
import { type ReportErrorConfig } from './report_error';
|
|
3
|
+
export interface WithErrorHandlingOptions {
|
|
4
|
+
/** Pass the relevant slices of your `RoutingConfig` directly — `{ errorHandling: config.errorHandling, generate: config.generate }`. */
|
|
5
|
+
config?: ReportErrorConfig;
|
|
6
|
+
params?: unknown;
|
|
7
|
+
isClient?: boolean;
|
|
8
|
+
consent?: ConsentValue;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wraps `fn`, reporting (via `options.config`) then rethrowing any error it
|
|
12
|
+
* throws or rejects with. Never swallows — callers keep their own
|
|
13
|
+
* catch/fallback behavior, this only adds reporting on top.
|
|
14
|
+
*/
|
|
15
|
+
export default function withErrorHandling<Args extends unknown[], Result>(fn: (...args: Args) => Result | Promise<Result>, classOrMethodName: string, options?: WithErrorHandlingOptions): (...args: Args) => Promise<Result>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import reportError from './report_error';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps `fn`, reporting (via `options.config`) then rethrowing any error it
|
|
4
|
+
* throws or rejects with. Never swallows — callers keep their own
|
|
5
|
+
* catch/fallback behavior, this only adds reporting on top.
|
|
6
|
+
*/
|
|
7
|
+
export default function withErrorHandling(fn, classOrMethodName, options = {}) {
|
|
8
|
+
const { config, params, isClient, consent } = options;
|
|
9
|
+
return async (...args) => {
|
|
10
|
+
try {
|
|
11
|
+
return await fn(...args);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
await reportError(config, { error, classOrMethodName, params, isClient, consent });
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -3,6 +3,7 @@ import { cache } from 'react';
|
|
|
3
3
|
import config from '@intl-config';
|
|
4
4
|
import requireFirebaseAuthConfig from '../require_config';
|
|
5
5
|
import { defaultSessionCookieName } from '../middleware/update_session';
|
|
6
|
+
import reportError from '../../error_handling/report_error';
|
|
6
7
|
let baseApp;
|
|
7
8
|
let firebaseAppModule;
|
|
8
9
|
let firebaseAuthModule;
|
|
@@ -51,7 +52,8 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
|
|
|
51
52
|
await auth.authStateReady();
|
|
52
53
|
return { firebaseServerApp, currentUser: auth.currentUser };
|
|
53
54
|
}
|
|
54
|
-
catch {
|
|
55
|
+
catch (error) {
|
|
56
|
+
await reportError(config, { error, classOrMethodName: 'getAuthenticatedAppForUser' });
|
|
55
57
|
return { firebaseServerApp: null, currentUser: null };
|
|
56
58
|
}
|
|
57
59
|
});
|
|
@@ -5,6 +5,8 @@ import dynamic from "next/dynamic";
|
|
|
5
5
|
import { localesSet } from "../../config/middleware";
|
|
6
6
|
import config from "../../config/intl_config";
|
|
7
7
|
import resolveRequiresConsent from "../../cookie_consent/gdpr_countries";
|
|
8
|
+
import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
|
|
9
|
+
import reportError from "../../error_handling/report_error";
|
|
8
10
|
const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider"));
|
|
9
11
|
let authUserServerProviderModule;
|
|
10
12
|
/**
|
|
@@ -48,6 +50,7 @@ export default async function LocationzationProvider({ language, messages, child
|
|
|
48
50
|
setMessageForLocaleCache(language, messages);
|
|
49
51
|
}
|
|
50
52
|
const messagesValue = messages ?? await getMessage(language);
|
|
53
|
+
installConsoleErrorOverride(config);
|
|
51
54
|
let initialAuthUser = null;
|
|
52
55
|
const autoWireClientProvider = config.firebaseAuth?.autoWireClientProvider !== false;
|
|
53
56
|
if (config.firebaseAuth && autoWireClientProvider) {
|
|
@@ -68,13 +71,21 @@ export default async function LocationzationProvider({ language, messages, child
|
|
|
68
71
|
// `getCloudflareContext` path in dev; fail-safe to `true`
|
|
69
72
|
// (banner shown) same as an unresolved country would.
|
|
70
73
|
requiresConsent = !isDevEnvironment
|
|
71
|
-
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.
|
|
74
|
+
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
|
|
72
75
|
: false;
|
|
73
76
|
const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
|
|
74
77
|
if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if (config.cookieConsent.getAnalytics) {
|
|
79
|
+
try {
|
|
80
|
+
analyticsConfig = await config.cookieConsent.getAnalytics();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await reportError(config, { error, classOrMethodName: 'getAnalytics' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
analyticsConfig = config.cookieConsent.analytics;
|
|
88
|
+
}
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, skipAuthProvider: !autoWireClientProvider, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
@@ -3,6 +3,7 @@ import type { Languages } from 'next/dist/lib/metadata/types/alternative-urls-ty
|
|
|
3
3
|
import type { Videos } from 'next/dist/lib/metadata/types/metadata-types';
|
|
4
4
|
import type { CookieConsentDialogProps } from '../cookie_consent/client/components/cookie_consent_dialog';
|
|
5
5
|
import type { PrivacyPolicyUpdateDialogProps } from '../cookie_consent/client/components/privacy_policy_update_dialog';
|
|
6
|
+
import type { ConsentValue } from '../cookie_consent/types';
|
|
6
7
|
/**
|
|
7
8
|
* Custom middleware hook, run by `intlMiddleware` for your own logic
|
|
8
9
|
* (e.g. auth, feature flags, A/B tests) — on top of the library's own
|
|
@@ -90,6 +91,108 @@ export interface RoutingConfig<AppLocales extends Locales, AppLocalePrefixMode e
|
|
|
90
91
|
* error if called without this set.
|
|
91
92
|
*/
|
|
92
93
|
cookieConsent?: CookieConsentRoutingConfig;
|
|
94
|
+
/**
|
|
95
|
+
* Request-time resolvers shared across submodules. Omit entirely to
|
|
96
|
+
* leave all of them unset.
|
|
97
|
+
*/
|
|
98
|
+
generate?: GenerateRoutingConfig;
|
|
99
|
+
/**
|
|
100
|
+
* Configures the optional `error_handling` submodule (shared
|
|
101
|
+
* `withErrorHandling`/`reportError` helpers used internally by this
|
|
102
|
+
* package and available to your own app code). Omit entirely to keep
|
|
103
|
+
* the defaults: enabled, reporting via `console.error`.
|
|
104
|
+
*/
|
|
105
|
+
errorHandling?: ErrorHandlingRoutingConfig;
|
|
106
|
+
}
|
|
107
|
+
export interface GenerateRoutingConfig {
|
|
108
|
+
/**
|
|
109
|
+
* Pass `getCloudflareContext` from `@opennextjs/cloudflare` directly
|
|
110
|
+
* (not a dependency of this package, so bring your own import) — its
|
|
111
|
+
* exact overloaded signature is accepted as-is; called internally with
|
|
112
|
+
* `{ async: true }`, so you never need to wrap it yourself. Only
|
|
113
|
+
* `cf.country` is read from the resolved context by `cookieConsent`.
|
|
114
|
+
*
|
|
115
|
+
* Country-based gating (via either `cookieConsent.getCountryCode` or
|
|
116
|
+
* this getter) decides whether the cookie-consent banner is required at
|
|
117
|
+
* all: visitors outside `gdprCountries` skip the banner and get
|
|
118
|
+
* analytics immediately (still gated by `enableAnalyticsInDevMode`).
|
|
119
|
+
* Omit BOTH to require consent for everyone (fail-safe default — the
|
|
120
|
+
* visitor's country can't be determined at all without either getter).
|
|
121
|
+
* Set one of the two getters to scope the banner to GDPR regions only.
|
|
122
|
+
*/
|
|
123
|
+
getCloudflareContext?: CookieConsentGetCloudflareContext;
|
|
124
|
+
}
|
|
125
|
+
export interface ErrorHandlingParams {
|
|
126
|
+
/** The caught error, in whatever shape it was thrown/rejected with. */
|
|
127
|
+
error: unknown;
|
|
128
|
+
/** Name of the function/method the error was caught in, e.g. `"resolveRequiresConsent"`. */
|
|
129
|
+
classOrMethodName: string;
|
|
130
|
+
/** Extra context to include in the report (arguments, request info, etc). */
|
|
131
|
+
params?: unknown;
|
|
132
|
+
/** Whether this error originated in a client-side (browser) call. */
|
|
133
|
+
isClient?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* The visitor's current cookie-consent value (from `useCookieConsent()`
|
|
136
|
+
* or your own server-side resolution), when known. When passed and not
|
|
137
|
+
* `true`, `reportError`/`withErrorHandling` skip reporting entirely —
|
|
138
|
+
* sending error reports to a third party (Telegram, Sentry, etc.)
|
|
139
|
+
* without consent can itself be GDPR-relevant. Omit when consent isn't
|
|
140
|
+
* applicable (e.g. `cookieConsent` isn't configured at all).
|
|
141
|
+
*/
|
|
142
|
+
consent?: ConsentValue;
|
|
143
|
+
/**
|
|
144
|
+
* Human-readable one-string summary — `[classOrMethodName] Error:
|
|
145
|
+
* <message>` plus non-empty `Params`/client-origin sections. Set by
|
|
146
|
+
* `reportError` before calling `onError`/`console.error`; read this
|
|
147
|
+
* instead of `error`/`params` directly when you just want something
|
|
148
|
+
* printable. Ignore when passing `params` to `withErrorHandling`
|
|
149
|
+
* yourself — it's always overwritten.
|
|
150
|
+
*/
|
|
151
|
+
formattedMessage?: string;
|
|
152
|
+
}
|
|
153
|
+
export interface ErrorHandlingRoutingConfig {
|
|
154
|
+
/**
|
|
155
|
+
* Whether errors caught by this package's `withErrorHandling`/
|
|
156
|
+
* `reportError` helpers are reported at all. Defaults to `true`. Set
|
|
157
|
+
* `false` to fully disable reporting (errors are still rethrown by
|
|
158
|
+
* `withErrorHandling`, just never reported).
|
|
159
|
+
*/
|
|
160
|
+
enable?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Called with the caught error whenever one is reported. Defaults to
|
|
163
|
+
* `console.error`. Use this to wire your own error-tracking/logging
|
|
164
|
+
* transport (Sentry, Telegram, etc).
|
|
165
|
+
*/
|
|
166
|
+
onError?: (params: ErrorHandlingParams) => void | Promise<void>;
|
|
167
|
+
/**
|
|
168
|
+
* Whether `reportError`/`withErrorHandling` replace the global
|
|
169
|
+
* `console.error` so every `console.error(...)` call in your app is
|
|
170
|
+
* also routed through `onError` (the original `console.error` still
|
|
171
|
+
* runs afterwards — nothing is swallowed). Defaults to `false`; call
|
|
172
|
+
* `installConsoleErrorOverride()` (or pass this `true` and call
|
|
173
|
+
* `IntlProvider`/`setIntlConfig`'s setup) to install it. Off by default
|
|
174
|
+
* since this package is shared across apps and a global override is a
|
|
175
|
+
* bigger behavior change than a plain function call.
|
|
176
|
+
*/
|
|
177
|
+
overrideConsoleError?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Substrings matched against the stringified message of each
|
|
180
|
+
* `console.error(...)` call (only consulted when `overrideConsoleError`
|
|
181
|
+
* is `true`) — a match skips reporting it (it's still logged normally).
|
|
182
|
+
* Defaults to `defaultIgnoredConsoleErrors` (this package's own Firebase
|
|
183
|
+
* Auth error codes for expected user-input failures — wrong password,
|
|
184
|
+
* email already in use, etc). Pass your own array to replace the
|
|
185
|
+
* default entirely; pass `[]` to report everything.
|
|
186
|
+
*/
|
|
187
|
+
ignoreConsoleErrors?: readonly string[];
|
|
188
|
+
/**
|
|
189
|
+
* Called with the stringified message of each `console.error(...)` call
|
|
190
|
+
* (only consulted when `overrideConsoleError` is `true`), in addition to
|
|
191
|
+
* `ignoreConsoleErrors` — return `true` to skip reporting it (it's still
|
|
192
|
+
* logged normally). Use this for custom filtering logic beyond a plain
|
|
193
|
+
* substring match.
|
|
194
|
+
*/
|
|
195
|
+
ignoreConsoleError?: (message: string) => boolean;
|
|
93
196
|
}
|
|
94
197
|
export interface CookieConsentRoutingConfig {
|
|
95
198
|
/**
|
|
@@ -145,27 +248,10 @@ export interface CookieConsentRoutingConfig {
|
|
|
145
248
|
* over `getCloudflareContext` when both are set.
|
|
146
249
|
*/
|
|
147
250
|
getCountryCode?: () => string | undefined | Promise<string | undefined>;
|
|
148
|
-
/**
|
|
149
|
-
* Pass `getCloudflareContext` from `@opennextjs/cloudflare` directly
|
|
150
|
-
* (not a dependency of this package, so bring your own import) — its
|
|
151
|
-
* exact overloaded signature is accepted as-is; called internally with
|
|
152
|
-
* `{ async: true }`, so you never need to wrap it yourself. Only
|
|
153
|
-
* `cf.country` is read from the resolved context. Ignored when
|
|
154
|
-
* `getCountryCode` is also set.
|
|
155
|
-
*
|
|
156
|
-
* Country-based gating (via either `getCountryCode` or
|
|
157
|
-
* `getCloudflareContext`) decides whether the cookie-consent banner is
|
|
158
|
-
* required at all: visitors outside `gdprCountries` skip the banner and
|
|
159
|
-
* get analytics immediately (still gated by `enableAnalyticsInDevMode`).
|
|
160
|
-
* Omit BOTH to require consent for everyone (fail-safe default — the
|
|
161
|
-
* visitor's country can't be determined at all without either getter).
|
|
162
|
-
* Set one of the two getters to scope the banner to GDPR regions only.
|
|
163
|
-
*/
|
|
164
|
-
getCloudflareContext?: CookieConsentGetCloudflareContext;
|
|
165
251
|
/**
|
|
166
252
|
* Country codes (ISO 3166-1 alpha-2) for which the cookie-consent banner
|
|
167
253
|
* is required. Only consulted when `getCountryCode` or
|
|
168
|
-
* `getCloudflareContext` is set. Defaults to the EU/EEA + UK +
|
|
254
|
+
* `generate.getCloudflareContext` is set. Defaults to the EU/EEA + UK +
|
|
169
255
|
* Switzerland (GDPR/UK-GDPR/nFADP scope). A visitor whose resolved
|
|
170
256
|
* country isn't in this set is treated as NOT requiring consent; a
|
|
171
257
|
* country that couldn't be resolved still requires it (fail-safe:
|
|
@@ -203,16 +289,21 @@ export interface CookieConsentRoutingConfig {
|
|
|
203
289
|
updateDialogProps?: PrivacyPolicyUpdateDialogProps;
|
|
204
290
|
}
|
|
205
291
|
/**
|
|
206
|
-
* Minimal shape read from your `getCloudflareContext()` return value
|
|
207
|
-
* `cf.country` is consulted (read defensively at the call
|
|
208
|
-
* real type — `@opennextjs/cloudflare`'s `CfProperties`,
|
|
209
|
-
* incoming-request and request-init variants — only has
|
|
210
|
-
* branch)
|
|
211
|
-
*
|
|
212
|
-
*
|
|
292
|
+
* Minimal shape read from your `getCloudflareContext()` return value.
|
|
293
|
+
* `cf.country` is consulted by `cookieConsent` (read defensively at the call
|
|
294
|
+
* site, since `cf`'s real type — `@opennextjs/cloudflare`'s `CfProperties`,
|
|
295
|
+
* a union of the incoming-request and request-init variants — only has
|
|
296
|
+
* `country` on one branch); `ctx.waitUntil` is used by `error_handling` to
|
|
297
|
+
* background error reports instead of awaiting them inline. Typed loosely
|
|
298
|
+
* here so the real (generic) function is assignable to
|
|
299
|
+
* `CookieConsentGetCloudflareContext` without a hard dependency on that
|
|
300
|
+
* package.
|
|
213
301
|
*/
|
|
214
302
|
export interface CookieConsentCloudflareContext {
|
|
215
303
|
cf?: Record<string, unknown>;
|
|
304
|
+
ctx?: {
|
|
305
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
306
|
+
};
|
|
216
307
|
}
|
|
217
308
|
/**
|
|
218
309
|
* Matches `@opennextjs/cloudflare`'s `getCloudflareContext` overloaded
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Optimized Next Intl Package Special for App Router and Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -139,6 +139,26 @@
|
|
|
139
139
|
"./cookieConsentAnalytics": {
|
|
140
140
|
"types": "./dist/src/cookie_consent/client/components/cookie_consent_analytics.d.ts",
|
|
141
141
|
"import": "./dist/src/cookie_consent/client/components/cookie_consent_analytics.js"
|
|
142
|
+
},
|
|
143
|
+
"./errorHandling": {
|
|
144
|
+
"types": "./dist/src/error_handling/index.d.ts",
|
|
145
|
+
"import": "./dist/src/error_handling/index.js"
|
|
146
|
+
},
|
|
147
|
+
"./installConsoleErrorOverride": {
|
|
148
|
+
"types": "./dist/src/error_handling/install_console_error_override.d.ts",
|
|
149
|
+
"import": "./dist/src/error_handling/install_console_error_override.js"
|
|
150
|
+
},
|
|
151
|
+
"./stringifyUnknown": {
|
|
152
|
+
"types": "./dist/src/error_handling/stringify_unknown.d.ts",
|
|
153
|
+
"import": "./dist/src/error_handling/stringify_unknown.js"
|
|
154
|
+
},
|
|
155
|
+
"./formatErrorMessage": {
|
|
156
|
+
"types": "./dist/src/error_handling/format_error_message.d.ts",
|
|
157
|
+
"import": "./dist/src/error_handling/format_error_message.js"
|
|
158
|
+
},
|
|
159
|
+
"./defaultIgnoredConsoleErrors": {
|
|
160
|
+
"types": "./dist/src/error_handling/default_ignored_console_errors.d.ts",
|
|
161
|
+
"import": "./dist/src/error_handling/default_ignored_console_errors.js"
|
|
142
162
|
}
|
|
143
163
|
},
|
|
144
164
|
"scripts": {
|