@feelflow/ffid-sdk 5.14.0 → 5.16.1

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 CHANGED
@@ -46,6 +46,10 @@ export default function RootLayout({ children }) {
46
46
  baseUrl={process.env.NEXT_PUBLIC_FFID_BASE_URL!}
47
47
  serviceApiKey={process.env.FFID_SERVICE_API_KEY!}
48
48
  gaMeasurementId={process.env.NEXT_PUBLIC_GA_ID ?? null}
49
+ // Production-only measurement (v5.16.0+): suppress GA on
50
+ // preview/staging/local so non-prod traffic never pollutes the
51
+ // production GA4 property. Defaults to true if omitted.
52
+ gaEnabled={process.env.VERCEL_ENV === 'production'}
49
53
  >
50
54
  {children}
51
55
  <FFIDCookieBanner />
@@ -393,13 +393,19 @@ interface FFIDConsentSyncResult {
393
393
  /**
394
394
  * Error code literal union for consent SDK failures.
395
395
  *
396
- * - The first 10 codes (`cookie_consent_*`) are reserved by spec §8.2 for
397
- * correlation across SDK / FFID API / ops dashboards.
396
+ * - The `cookie_consent_*` codes reserved by spec §8.2 correlate across
397
+ * SDK / FFID API / ops dashboards.
398
+ * - `cookie_consent_aborted` is an SDK-only code (NOT spec §8.2 reserved):
399
+ * the in-flight fetch was cancelled via an `AbortSignal` (e.g. component
400
+ * unmount or page navigation). It is a benign teardown, not a backend
401
+ * failure, so consumers should generally NOT surface it as an error — it is
402
+ * kept distinct from `cookie_consent_api_unreachable` precisely so the
403
+ * provider can swallow it instead of emitting Sentry noise (#3869).
398
404
  * - `auth_failure` is added by the SDK to cover 401 / 403 responses; it is
399
405
  * NOT part of spec §8.2 reserved codes — the SDK shares this code with the
400
406
  * wider FFID auth surface so consumer error handlers can branch uniformly.
401
407
  */
402
- declare const FFID_CONSENT_ERROR_CODES: readonly ["cookie_consent_api_unreachable", "cookie_consent_response_invalid", "cookie_consent_rate_limited", "cookie_consent_cookie_write_failed", "cookie_consent_parse_failed", "cookie_consent_sync_conflict", "cookie_consent_immutable_violation", "cookie_consent_policy_outdated", "cookie_consent_capture_failed", "cookie_consent_db_write_failed", "auth_failure"];
408
+ declare const FFID_CONSENT_ERROR_CODES: readonly ["cookie_consent_api_unreachable", "cookie_consent_aborted", "cookie_consent_response_invalid", "cookie_consent_rate_limited", "cookie_consent_cookie_write_failed", "cookie_consent_parse_failed", "cookie_consent_sync_conflict", "cookie_consent_immutable_violation", "cookie_consent_policy_outdated", "cookie_consent_capture_failed", "cookie_consent_db_write_failed", "auth_failure"];
403
409
  type FFIDConsentErrorCode = (typeof FFID_CONSENT_ERROR_CODES)[number];
404
410
  /**
405
411
  * Error class for all consent-related failures surfaced through SDK callbacks
@@ -615,6 +621,16 @@ declare global {
615
621
  interface GtagBridgeOptions {
616
622
  /** GA Measurement ID, e.g., `G-XXXXXXX`. Empty/null = GA not wired. */
617
623
  gaMeasurementId?: string | null;
624
+ /**
625
+ * Master switch for GA tag injection. When `false`, the bridge still manages
626
+ * Consent Mode dataLayer pushes (`setDefaultConsent` / `updateConsent`) but
627
+ * `applyGaInjection` never appends the `gtag.js` script — regardless of
628
+ * `gaMeasurementId` or analytics consent. Defaults to `true` (legacy
629
+ * behavior). Consumers gate this on their own environment so non-production
630
+ * builds don't pollute the production GA4 property (e.g.
631
+ * `gaEnabled={process.env.VERCEL_ENV === 'production'}`).
632
+ */
633
+ gaEnabled?: boolean;
618
634
  /** Override script src for testing. Default: official gtag URL. */
619
635
  gtagScriptSrc?: string;
620
636
  /** Document/window injection target (server-side stays no-op). */
@@ -703,6 +719,23 @@ interface FFIDAnalyticsProviderProps {
703
719
  * avoiding the triple-state `string | null | undefined` anti-pattern.
704
720
  */
705
721
  gaMeasurementId?: string | null;
722
+ /**
723
+ * Master switch for GA tag injection. When `false`, the provider still runs
724
+ * Consent Mode (default/update dataLayer pushes) but never injects the
725
+ * `gtag.js` script — regardless of `gaMeasurementId` or analytics consent.
726
+ * Defaults to `true` (legacy behavior, fully backward compatible).
727
+ *
728
+ * Gate this on your own deploy environment so non-production builds don't
729
+ * send hits to the production GA4 property, e.g.
730
+ * `gaEnabled={process.env.VERCEL_ENV === 'production'}`. Read the env signal
731
+ * server-side where possible (it is captured once at mount, not reactive).
732
+ *
733
+ * Prefer `gaEnabled={false}` over `gaMeasurementId={null}` when gating by
734
+ * environment: `gaEnabled={false}` keeps a stable measurement ID wired across
735
+ * builds and keeps Consent Mode pushes flowing, whereas `null` means "GA was
736
+ * never configured for this app".
737
+ */
738
+ gaEnabled?: boolean;
706
739
  /** When true, control Sentry session replay based on `functional` consent. */
707
740
  sentryReplay?: boolean;
708
741
  /** Sentry namespace override; defaults to `(window as any).Sentry`. */
@@ -751,7 +784,7 @@ interface FFIDAnalyticsProviderProps {
751
784
  fetchImpl?: typeof fetch;
752
785
  children: ReactNode;
753
786
  }
754
- declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, mergeWarningMessages, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
787
+ declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, gaEnabled, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, mergeWarningMessages, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
755
788
 
756
789
  interface FFIDConsentContextValue {
757
790
  /** Current consent state. Always non-null; "not yet decided" is encoded by `hasDecided=false`. */
@@ -393,13 +393,19 @@ interface FFIDConsentSyncResult {
393
393
  /**
394
394
  * Error code literal union for consent SDK failures.
395
395
  *
396
- * - The first 10 codes (`cookie_consent_*`) are reserved by spec §8.2 for
397
- * correlation across SDK / FFID API / ops dashboards.
396
+ * - The `cookie_consent_*` codes reserved by spec §8.2 correlate across
397
+ * SDK / FFID API / ops dashboards.
398
+ * - `cookie_consent_aborted` is an SDK-only code (NOT spec §8.2 reserved):
399
+ * the in-flight fetch was cancelled via an `AbortSignal` (e.g. component
400
+ * unmount or page navigation). It is a benign teardown, not a backend
401
+ * failure, so consumers should generally NOT surface it as an error — it is
402
+ * kept distinct from `cookie_consent_api_unreachable` precisely so the
403
+ * provider can swallow it instead of emitting Sentry noise (#3869).
398
404
  * - `auth_failure` is added by the SDK to cover 401 / 403 responses; it is
399
405
  * NOT part of spec §8.2 reserved codes — the SDK shares this code with the
400
406
  * wider FFID auth surface so consumer error handlers can branch uniformly.
401
407
  */
402
- declare const FFID_CONSENT_ERROR_CODES: readonly ["cookie_consent_api_unreachable", "cookie_consent_response_invalid", "cookie_consent_rate_limited", "cookie_consent_cookie_write_failed", "cookie_consent_parse_failed", "cookie_consent_sync_conflict", "cookie_consent_immutable_violation", "cookie_consent_policy_outdated", "cookie_consent_capture_failed", "cookie_consent_db_write_failed", "auth_failure"];
408
+ declare const FFID_CONSENT_ERROR_CODES: readonly ["cookie_consent_api_unreachable", "cookie_consent_aborted", "cookie_consent_response_invalid", "cookie_consent_rate_limited", "cookie_consent_cookie_write_failed", "cookie_consent_parse_failed", "cookie_consent_sync_conflict", "cookie_consent_immutable_violation", "cookie_consent_policy_outdated", "cookie_consent_capture_failed", "cookie_consent_db_write_failed", "auth_failure"];
403
409
  type FFIDConsentErrorCode = (typeof FFID_CONSENT_ERROR_CODES)[number];
404
410
  /**
405
411
  * Error class for all consent-related failures surfaced through SDK callbacks
@@ -615,6 +621,16 @@ declare global {
615
621
  interface GtagBridgeOptions {
616
622
  /** GA Measurement ID, e.g., `G-XXXXXXX`. Empty/null = GA not wired. */
617
623
  gaMeasurementId?: string | null;
624
+ /**
625
+ * Master switch for GA tag injection. When `false`, the bridge still manages
626
+ * Consent Mode dataLayer pushes (`setDefaultConsent` / `updateConsent`) but
627
+ * `applyGaInjection` never appends the `gtag.js` script — regardless of
628
+ * `gaMeasurementId` or analytics consent. Defaults to `true` (legacy
629
+ * behavior). Consumers gate this on their own environment so non-production
630
+ * builds don't pollute the production GA4 property (e.g.
631
+ * `gaEnabled={process.env.VERCEL_ENV === 'production'}`).
632
+ */
633
+ gaEnabled?: boolean;
618
634
  /** Override script src for testing. Default: official gtag URL. */
619
635
  gtagScriptSrc?: string;
620
636
  /** Document/window injection target (server-side stays no-op). */
@@ -703,6 +719,23 @@ interface FFIDAnalyticsProviderProps {
703
719
  * avoiding the triple-state `string | null | undefined` anti-pattern.
704
720
  */
705
721
  gaMeasurementId?: string | null;
722
+ /**
723
+ * Master switch for GA tag injection. When `false`, the provider still runs
724
+ * Consent Mode (default/update dataLayer pushes) but never injects the
725
+ * `gtag.js` script — regardless of `gaMeasurementId` or analytics consent.
726
+ * Defaults to `true` (legacy behavior, fully backward compatible).
727
+ *
728
+ * Gate this on your own deploy environment so non-production builds don't
729
+ * send hits to the production GA4 property, e.g.
730
+ * `gaEnabled={process.env.VERCEL_ENV === 'production'}`. Read the env signal
731
+ * server-side where possible (it is captured once at mount, not reactive).
732
+ *
733
+ * Prefer `gaEnabled={false}` over `gaMeasurementId={null}` when gating by
734
+ * environment: `gaEnabled={false}` keeps a stable measurement ID wired across
735
+ * builds and keeps Consent Mode pushes flowing, whereas `null` means "GA was
736
+ * never configured for this app".
737
+ */
738
+ gaEnabled?: boolean;
706
739
  /** When true, control Sentry session replay based on `functional` consent. */
707
740
  sentryReplay?: boolean;
708
741
  /** Sentry namespace override; defaults to `(window as any).Sentry`. */
@@ -751,7 +784,7 @@ interface FFIDAnalyticsProviderProps {
751
784
  fetchImpl?: typeof fetch;
752
785
  children: ReactNode;
753
786
  }
754
- declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, mergeWarningMessages, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
787
+ declare function FFIDAnalyticsProvider({ baseUrl, serviceApiKey, getAccessToken, gaMeasurementId, gaEnabled, sentryReplay, sentry, onConsentChange, onError, autoShowBanner, dismissalCooldownMinutes, mergeWarningMessages, fetchImpl, children, }: FFIDAnalyticsProviderProps): React.JSX.Element;
755
788
 
756
789
  interface FFIDConsentContextValue {
757
790
  /** Current consent state. Always non-null; "not yet decided" is encoded by `hasDecided=false`. */
@@ -160,6 +160,7 @@ var ErrorBodyWireSchema = zod.z.object({
160
160
  var FFID_CONSENT_ERROR_CODES = [
161
161
  // SDK client (network / response)
162
162
  "cookie_consent_api_unreachable",
163
+ "cookie_consent_aborted",
163
164
  "cookie_consent_response_invalid",
164
165
  "cookie_consent_rate_limited",
165
166
  // SDK storage
@@ -193,6 +194,7 @@ var FFIDConsentError = class extends Error {
193
194
  };
194
195
  var DEFAULT_CONSENT_ERROR_MESSAGES = {
195
196
  cookie_consent_api_unreachable: "\u901A\u4FE1\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
197
+ cookie_consent_aborted: "\u901A\u4FE1\u304C\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
196
198
  cookie_consent_response_invalid: "\u540C\u610F\u60C5\u5831\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u6642\u9593\u3092\u7F6E\u3044\u3066\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
197
199
  cookie_consent_rate_limited: "\u30B5\u30FC\u30D3\u30B9\u304C\u6DF7\u96D1\u3057\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u7D4C\u3063\u3066\u304B\u3089\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
198
200
  cookie_consent_cookie_write_failed: "\u30D6\u30E9\u30A6\u30B6\u306E\u8A2D\u5B9A\u306B\u3088\u308A\u540C\u610F\u60C5\u5831\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8\u30D6\u30E9\u30A6\u30BA\u3084\u62E1\u5F35\u6A5F\u80FD\u3092\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044",
@@ -658,6 +660,9 @@ function jitteredDelayMs(baseSec) {
658
660
  const jitter = (Math.random() * 2 - 1) * RETRY_JITTER_RATIO;
659
661
  return Math.max(0, baseSec * MS_PER_SECOND * (1 + jitter));
660
662
  }
663
+ function isAbortError(cause) {
664
+ return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "AbortError";
665
+ }
661
666
  function parseErrorBody(json, retryAfterHeader) {
662
667
  const parsed = ErrorBodyWireSchema.safeParse(json);
663
668
  const body = parsed.success ? parsed.data : {};
@@ -767,6 +772,15 @@ function createConsentClient(opts) {
767
772
  const res = await fetchImpl(url, init);
768
773
  return { res };
769
774
  } catch (cause) {
775
+ if (isAbortError(cause)) {
776
+ return {
777
+ err: new FFIDConsentError(
778
+ "cookie_consent_aborted",
779
+ `Request to ${url} was aborted`,
780
+ { cause }
781
+ )
782
+ };
783
+ }
770
784
  return {
771
785
  err: new FFIDConsentError(
772
786
  "cookie_consent_api_unreachable",
@@ -980,16 +994,27 @@ function mapCategoriesToGtagParams(cats) {
980
994
  ad_personalization: bool(cats.marketing)
981
995
  };
982
996
  }
983
- function pushDataLayer(win, args) {
984
- if (!win) return;
985
- win.dataLayer = win.dataLayer ?? [];
986
- win.dataLayer.push(args);
997
+ function ensureGtag(win) {
998
+ const dataLayer = win.dataLayer = win.dataLayer ?? [];
999
+ let fn = win.gtag;
1000
+ if (typeof fn !== "function") {
1001
+ fn = function gtag() {
1002
+ dataLayer.push(arguments);
1003
+ };
1004
+ win.gtag = fn;
1005
+ }
1006
+ return fn;
987
1007
  }
988
1008
  function createGtagBridge(opts = {}) {
989
1009
  const win = opts.win ?? (typeof window !== "undefined" ? window : void 0);
990
1010
  const gaId = opts.gaMeasurementId ?? null;
1011
+ const gaEnabled = opts.gaEnabled ?? true;
991
1012
  const scriptSrc = opts.gtagScriptSrc ?? DEFAULT_GTAG_SRC;
992
1013
  let gaScriptInjected = false;
1014
+ function command(...args) {
1015
+ if (!win) return;
1016
+ ensureGtag(win)(...args);
1017
+ }
993
1018
  return {
994
1019
  setDefaultConsent() {
995
1020
  const denyParams = mapCategoriesToGtagParams({
@@ -997,13 +1022,13 @@ function createGtagBridge(opts = {}) {
997
1022
  analytics: false,
998
1023
  marketing: false
999
1024
  });
1000
- pushDataLayer(win, ["consent", "default", denyParams]);
1025
+ command("consent", "default", denyParams);
1001
1026
  },
1002
1027
  updateConsent(cats) {
1003
- const params = mapCategoriesToGtagParams(cats);
1004
- pushDataLayer(win, ["consent", "update", params]);
1028
+ command("consent", "update", mapCategoriesToGtagParams(cats));
1005
1029
  },
1006
1030
  applyGaInjection(cats) {
1031
+ if (!gaEnabled) return;
1007
1032
  if (!win || !gaId) return;
1008
1033
  if (!cats.analytics) return;
1009
1034
  if (gaScriptInjected) return;
@@ -1013,8 +1038,8 @@ function createGtagBridge(opts = {}) {
1013
1038
  script.async = true;
1014
1039
  script.src = `${scriptSrc}?id=${encodeURIComponent(gaId)}`;
1015
1040
  doc.head.appendChild(script);
1016
- pushDataLayer(win, ["js", /* @__PURE__ */ new Date()]);
1017
- pushDataLayer(win, ["config", gaId]);
1041
+ command("js", /* @__PURE__ */ new Date());
1042
+ command("config", gaId);
1018
1043
  gaScriptInjected = true;
1019
1044
  }
1020
1045
  };
@@ -1072,6 +1097,7 @@ function FFIDAnalyticsProvider({
1072
1097
  serviceApiKey,
1073
1098
  getAccessToken,
1074
1099
  gaMeasurementId,
1100
+ gaEnabled,
1075
1101
  sentryReplay,
1076
1102
  sentry,
1077
1103
  onConsentChange,
@@ -1120,9 +1146,11 @@ function FFIDAnalyticsProvider({
1120
1146
  clientRef.current = createConsentClient(clientOpts);
1121
1147
  }
1122
1148
  if (!bridgeRef.current) {
1123
- bridgeRef.current = createGtagBridge({
1149
+ const bridgeOpts = {
1124
1150
  gaMeasurementId: gaMeasurementId ?? null
1125
- });
1151
+ };
1152
+ if (gaEnabled !== void 0) bridgeOpts.gaEnabled = gaEnabled;
1153
+ bridgeRef.current = createGtagBridge(bridgeOpts);
1126
1154
  bridgeRef.current.setDefaultConsent();
1127
1155
  }
1128
1156
  const applyCategoriesSideEffects = react.useCallback(
@@ -1189,14 +1217,16 @@ function FFIDAnalyticsProvider({
1189
1217
  );
1190
1218
  react.useEffect(() => {
1191
1219
  let cancelled = false;
1220
+ const controller = new AbortController();
1192
1221
  async function bootstrap() {
1193
1222
  const local = readConsentSessionMirror() ?? readConsentCookie();
1194
1223
  applyCategoriesSideEffects(local);
1195
1224
  setState(stateFromLocalCategories(local));
1196
1225
  const client = clientRef.current;
1197
- const result = await client.getMe();
1226
+ const result = await client.getMe(controller.signal);
1198
1227
  if (cancelled) return;
1199
1228
  if (!result.ok) {
1229
+ if (result.error.code === "cookie_consent_aborted") return;
1200
1230
  setError(result.error);
1201
1231
  onError?.(result.error);
1202
1232
  setIsLoading(false);
@@ -1220,6 +1250,7 @@ function FFIDAnalyticsProvider({
1220
1250
  void bootstrap();
1221
1251
  return () => {
1222
1252
  cancelled = true;
1253
+ controller.abort();
1223
1254
  };
1224
1255
  }, []);
1225
1256
  react.useEffect(() => {
@@ -158,6 +158,7 @@ var ErrorBodyWireSchema = z.object({
158
158
  var FFID_CONSENT_ERROR_CODES = [
159
159
  // SDK client (network / response)
160
160
  "cookie_consent_api_unreachable",
161
+ "cookie_consent_aborted",
161
162
  "cookie_consent_response_invalid",
162
163
  "cookie_consent_rate_limited",
163
164
  // SDK storage
@@ -191,6 +192,7 @@ var FFIDConsentError = class extends Error {
191
192
  };
192
193
  var DEFAULT_CONSENT_ERROR_MESSAGES = {
193
194
  cookie_consent_api_unreachable: "\u901A\u4FE1\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
195
+ cookie_consent_aborted: "\u901A\u4FE1\u304C\u4E2D\u65AD\u3055\u308C\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
194
196
  cookie_consent_response_invalid: "\u540C\u610F\u60C5\u5831\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u6642\u9593\u3092\u7F6E\u3044\u3066\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
195
197
  cookie_consent_rate_limited: "\u30B5\u30FC\u30D3\u30B9\u304C\u6DF7\u96D1\u3057\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u7D4C\u3063\u3066\u304B\u3089\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
196
198
  cookie_consent_cookie_write_failed: "\u30D6\u30E9\u30A6\u30B6\u306E\u8A2D\u5B9A\u306B\u3088\u308A\u540C\u610F\u60C5\u5831\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30D7\u30E9\u30A4\u30D9\u30FC\u30C8\u30D6\u30E9\u30A6\u30BA\u3084\u62E1\u5F35\u6A5F\u80FD\u3092\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044",
@@ -656,6 +658,9 @@ function jitteredDelayMs(baseSec) {
656
658
  const jitter = (Math.random() * 2 - 1) * RETRY_JITTER_RATIO;
657
659
  return Math.max(0, baseSec * MS_PER_SECOND * (1 + jitter));
658
660
  }
661
+ function isAbortError(cause) {
662
+ return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "AbortError";
663
+ }
659
664
  function parseErrorBody(json, retryAfterHeader) {
660
665
  const parsed = ErrorBodyWireSchema.safeParse(json);
661
666
  const body = parsed.success ? parsed.data : {};
@@ -765,6 +770,15 @@ function createConsentClient(opts) {
765
770
  const res = await fetchImpl(url, init);
766
771
  return { res };
767
772
  } catch (cause) {
773
+ if (isAbortError(cause)) {
774
+ return {
775
+ err: new FFIDConsentError(
776
+ "cookie_consent_aborted",
777
+ `Request to ${url} was aborted`,
778
+ { cause }
779
+ )
780
+ };
781
+ }
768
782
  return {
769
783
  err: new FFIDConsentError(
770
784
  "cookie_consent_api_unreachable",
@@ -978,16 +992,27 @@ function mapCategoriesToGtagParams(cats) {
978
992
  ad_personalization: bool(cats.marketing)
979
993
  };
980
994
  }
981
- function pushDataLayer(win, args) {
982
- if (!win) return;
983
- win.dataLayer = win.dataLayer ?? [];
984
- win.dataLayer.push(args);
995
+ function ensureGtag(win) {
996
+ const dataLayer = win.dataLayer = win.dataLayer ?? [];
997
+ let fn = win.gtag;
998
+ if (typeof fn !== "function") {
999
+ fn = function gtag() {
1000
+ dataLayer.push(arguments);
1001
+ };
1002
+ win.gtag = fn;
1003
+ }
1004
+ return fn;
985
1005
  }
986
1006
  function createGtagBridge(opts = {}) {
987
1007
  const win = opts.win ?? (typeof window !== "undefined" ? window : void 0);
988
1008
  const gaId = opts.gaMeasurementId ?? null;
1009
+ const gaEnabled = opts.gaEnabled ?? true;
989
1010
  const scriptSrc = opts.gtagScriptSrc ?? DEFAULT_GTAG_SRC;
990
1011
  let gaScriptInjected = false;
1012
+ function command(...args) {
1013
+ if (!win) return;
1014
+ ensureGtag(win)(...args);
1015
+ }
991
1016
  return {
992
1017
  setDefaultConsent() {
993
1018
  const denyParams = mapCategoriesToGtagParams({
@@ -995,13 +1020,13 @@ function createGtagBridge(opts = {}) {
995
1020
  analytics: false,
996
1021
  marketing: false
997
1022
  });
998
- pushDataLayer(win, ["consent", "default", denyParams]);
1023
+ command("consent", "default", denyParams);
999
1024
  },
1000
1025
  updateConsent(cats) {
1001
- const params = mapCategoriesToGtagParams(cats);
1002
- pushDataLayer(win, ["consent", "update", params]);
1026
+ command("consent", "update", mapCategoriesToGtagParams(cats));
1003
1027
  },
1004
1028
  applyGaInjection(cats) {
1029
+ if (!gaEnabled) return;
1005
1030
  if (!win || !gaId) return;
1006
1031
  if (!cats.analytics) return;
1007
1032
  if (gaScriptInjected) return;
@@ -1011,8 +1036,8 @@ function createGtagBridge(opts = {}) {
1011
1036
  script.async = true;
1012
1037
  script.src = `${scriptSrc}?id=${encodeURIComponent(gaId)}`;
1013
1038
  doc.head.appendChild(script);
1014
- pushDataLayer(win, ["js", /* @__PURE__ */ new Date()]);
1015
- pushDataLayer(win, ["config", gaId]);
1039
+ command("js", /* @__PURE__ */ new Date());
1040
+ command("config", gaId);
1016
1041
  gaScriptInjected = true;
1017
1042
  }
1018
1043
  };
@@ -1070,6 +1095,7 @@ function FFIDAnalyticsProvider({
1070
1095
  serviceApiKey,
1071
1096
  getAccessToken,
1072
1097
  gaMeasurementId,
1098
+ gaEnabled,
1073
1099
  sentryReplay,
1074
1100
  sentry,
1075
1101
  onConsentChange,
@@ -1118,9 +1144,11 @@ function FFIDAnalyticsProvider({
1118
1144
  clientRef.current = createConsentClient(clientOpts);
1119
1145
  }
1120
1146
  if (!bridgeRef.current) {
1121
- bridgeRef.current = createGtagBridge({
1147
+ const bridgeOpts = {
1122
1148
  gaMeasurementId: gaMeasurementId ?? null
1123
- });
1149
+ };
1150
+ if (gaEnabled !== void 0) bridgeOpts.gaEnabled = gaEnabled;
1151
+ bridgeRef.current = createGtagBridge(bridgeOpts);
1124
1152
  bridgeRef.current.setDefaultConsent();
1125
1153
  }
1126
1154
  const applyCategoriesSideEffects = useCallback(
@@ -1187,14 +1215,16 @@ function FFIDAnalyticsProvider({
1187
1215
  );
1188
1216
  useEffect(() => {
1189
1217
  let cancelled = false;
1218
+ const controller = new AbortController();
1190
1219
  async function bootstrap() {
1191
1220
  const local = readConsentSessionMirror() ?? readConsentCookie();
1192
1221
  applyCategoriesSideEffects(local);
1193
1222
  setState(stateFromLocalCategories(local));
1194
1223
  const client = clientRef.current;
1195
- const result = await client.getMe();
1224
+ const result = await client.getMe(controller.signal);
1196
1225
  if (cancelled) return;
1197
1226
  if (!result.ok) {
1227
+ if (result.error.code === "cookie_consent_aborted") return;
1198
1228
  setError(result.error);
1199
1229
  onError?.(result.error);
1200
1230
  setIsLoading(false);
@@ -1218,6 +1248,7 @@ function FFIDAnalyticsProvider({
1218
1248
  void bootstrap();
1219
1249
  return () => {
1220
1250
  cancelled = true;
1251
+ controller.abort();
1221
1252
  };
1222
1253
  }, []);
1223
1254
  useEffect(() => {
@@ -1095,7 +1095,7 @@ function createNonContractMethods(deps) {
1095
1095
  }
1096
1096
 
1097
1097
  // src/client/version-check.ts
1098
- var SDK_VERSION = "5.14.0";
1098
+ var SDK_VERSION = "5.16.1";
1099
1099
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1100
1100
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1101
1101
  function sdkHeaders() {
@@ -1093,7 +1093,7 @@ function createNonContractMethods(deps) {
1093
1093
  }
1094
1094
 
1095
1095
  // src/client/version-check.ts
1096
- var SDK_VERSION = "5.14.0";
1096
+ var SDK_VERSION = "5.16.1";
1097
1097
  var SDK_USER_AGENT = `FFID-SDK/${SDK_VERSION} (TypeScript)`;
1098
1098
  var SDK_VERSION_HEADER = "X-FFID-SDK-Version";
1099
1099
  function sdkHeaders() {
@@ -1,34 +1,34 @@
1
1
  'use strict';
2
2
 
3
- var chunkBVDUQQHP_cjs = require('../chunk-BVDUQQHP.cjs');
3
+ var chunkSNSIVY3Q_cjs = require('../chunk-SNSIVY3Q.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "FFIDAnnouncementBadge", {
8
8
  enumerable: true,
9
- get: function () { return chunkBVDUQQHP_cjs.FFIDAnnouncementBadge; }
9
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDAnnouncementBadge; }
10
10
  });
11
11
  Object.defineProperty(exports, "FFIDAnnouncementList", {
12
12
  enumerable: true,
13
- get: function () { return chunkBVDUQQHP_cjs.FFIDAnnouncementList; }
13
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDAnnouncementList; }
14
14
  });
15
15
  Object.defineProperty(exports, "FFIDInquiryForm", {
16
16
  enumerable: true,
17
- get: function () { return chunkBVDUQQHP_cjs.FFIDInquiryForm; }
17
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDInquiryForm; }
18
18
  });
19
19
  Object.defineProperty(exports, "FFIDLoginButton", {
20
20
  enumerable: true,
21
- get: function () { return chunkBVDUQQHP_cjs.FFIDLoginButton; }
21
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDLoginButton; }
22
22
  });
23
23
  Object.defineProperty(exports, "FFIDOrganizationSwitcher", {
24
24
  enumerable: true,
25
- get: function () { return chunkBVDUQQHP_cjs.FFIDOrganizationSwitcher; }
25
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDOrganizationSwitcher; }
26
26
  });
27
27
  Object.defineProperty(exports, "FFIDSubscriptionBadge", {
28
28
  enumerable: true,
29
- get: function () { return chunkBVDUQQHP_cjs.FFIDSubscriptionBadge; }
29
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDSubscriptionBadge; }
30
30
  });
31
31
  Object.defineProperty(exports, "FFIDUserMenu", {
32
32
  enumerable: true,
33
- get: function () { return chunkBVDUQQHP_cjs.FFIDUserMenu; }
33
+ get: function () { return chunkSNSIVY3Q_cjs.FFIDUserMenu; }
34
34
  });
@@ -1 +1 @@
1
- export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-4II4R7NR.js';
1
+ export { FFIDAnnouncementBadge, FFIDAnnouncementList, FFIDInquiryForm, FFIDLoginButton, FFIDOrganizationSwitcher, FFIDSubscriptionBadge, FFIDUserMenu } from '../chunk-XT4BAOKZ.js';