@compilacion/colleciones-clientos 2.0.29 → 2.0.32

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.
@@ -10,12 +10,13 @@ var collecionesClientosGtmConfigSandbox = (function (exports) {
10
10
  'trackerName': 'trackerName',
11
11
  'appName': 'appName',
12
12
  'emitterEndpoint': 'emitterEndpoint',
13
+ 'errorLoggingEndpoint': 'errorLoggingEndpoint',
13
14
  'typeName': 'type'
14
15
  },
15
16
  'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'
16
17
  };
17
18
 
18
- function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput) {
19
+ function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput, errorLoggingEndpoint) {
19
20
  let returnObject = {};
20
21
  returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;
21
22
  returnObject[constants.config.emitterEndpoint] = endpoint;
@@ -25,9 +26,12 @@ var collecionesClientosGtmConfigSandbox = (function (exports) {
25
26
  returnObject[constants.config.appName] = appName;
26
27
  returnObject[constants.config.sourceName] = jsLibSource;
27
28
  returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;
29
+ // Optional: providing the URL of the server-side error-logging endpoint is what
30
+ // turns frontend error forwarding on. When left empty the runtime stays silent.
31
+ returnObject[constants.config.errorLoggingEndpoint] = errorLoggingEndpoint;
28
32
  return returnObject;
29
33
  }
30
- /* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl);*/
34
+ /* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl, errorLoggingEndpoint);*/
31
35
 
32
36
  exports.getConfigObject = getConfigObject;
33
37
 
@@ -1 +1 @@
1
- {"version":3,"file":"browser.domainModel.gtm.config.sandbox.js","sources":["../src/browser.domainModel.gtm.constants.js","../src/browser.domainModel.gtm.config.sandbox.js"],"sourcesContent":["var constants = {\n 'sharedWindowObjectNames': {\n 'queue': 'compilacionCollecionesClientosGtmQueue',\n 'configVariable': 'compilacionCollecionesClientosGtmTypeCastConfig'\n },\n 'config': {\n 'sourceName': 'jsLibSource',\n 'jsLibUrlVariable': 'jsLibSource_selfHostedUrl',\n 'selfHostedValue': 'SELF',\n 'unpkgHostedValue': 'UNPKG',\n 'flushInterval': 'flushInterval',\n 'flushSize': 'flushSize',\n 'trackerName': 'trackerName',\n 'appName': 'appName',\n 'emitterEndpoint': 'emitterEndpoint',\n 'typeName': 'type'\n },\n 'collecionesObject': 'compilacionCollecionesClientosGtm',\n 'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'\n};\n\nexport default constants;","import constants from './browser.domainModel.gtm.constants.js';\n\nexport function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput) { \n let jsLibUrl = null;\n if(jsLibSource === constants.config.unpkgHostedValue) {\n jsLibUrl = \"https://unpkg.com/@compilacion/colleciones-clientos@latest/dist/browser.gtm.min.js\";\n } else if (jsLibSource === constants.config.selfHostedValue) {\n jsLibUrl = jsLibUrlInput\n }\n let returnObject = {};\n returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;\n returnObject[constants.config.emitterEndpoint] = endpoint;\n returnObject[constants.config.flushInterval] = flushInterval;\n returnObject[constants.config.flushSize] = flushSize;\n returnObject[constants.config.trackerName] = trackerName;\n returnObject[constants.config.appName] = appName;\n returnObject[constants.config.sourceName] = jsLibSource;\n returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;\n return returnObject;\n}\n/* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl);*/"],"names":[],"mappings":";;;IAAA,IAAI,SAAS,GAAG;IAChB,IAII,QAAQ,EAAE;IACd,QAAQ,YAAY,EAAE,aAAa;IACnC,QAAQ,kBAAkB,EAAE,2BAA2B;IACvD,QAEQ,eAAe,EAAE,eAAe;IACxC,QAAQ,WAAW,EAAE,WAAW;IAChC,QAAQ,aAAa,EAAE,aAAa;IACpC,QAAQ,SAAS,EAAE,SAAS;IAC5B,QAAQ,iBAAiB,EAAE,iBAAiB;IAC5C,QAAQ,UAAU,EAAE;IACpB,KAAK;IACL,IACI,2BAA2B,EAAE;IACjC,CAAC;;ICjBM,SAAS,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE;IAOtH,IAAI,IAAI,YAAY,GAAG,EAAE;IACzB,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,yBAAyB;IACjF,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,QAAQ;IAC7D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,aAAa;IAChE,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS;IACxD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,WAAW;IAC5D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO;IACpD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,WAAW;IAC3D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,aAAa;IACnE,IAAI,OAAO,YAAY;IACvB;IACA;;;;;;;;;;"}
1
+ {"version":3,"file":"browser.domainModel.gtm.config.sandbox.js","sources":["../src/browser.domainModel.gtm.constants.js","../src/browser.domainModel.gtm.config.sandbox.js"],"sourcesContent":["var constants = {\n 'sharedWindowObjectNames': {\n 'queue': 'compilacionCollecionesClientosGtmQueue',\n 'configVariable': 'compilacionCollecionesClientosGtmTypeCastConfig'\n },\n 'config': {\n 'sourceName': 'jsLibSource',\n 'jsLibUrlVariable': 'jsLibSource_selfHostedUrl',\n 'selfHostedValue': 'SELF',\n 'unpkgHostedValue': 'UNPKG',\n 'flushInterval': 'flushInterval',\n 'flushSize': 'flushSize',\n 'trackerName': 'trackerName',\n 'appName': 'appName',\n 'emitterEndpoint': 'emitterEndpoint',\n 'errorLoggingEndpoint': 'errorLoggingEndpoint',\n 'typeName': 'type'\n },\n 'collecionesObject': 'compilacionCollecionesClientosGtm',\n 'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'\n};\n\nexport default constants;","import constants from './browser.domainModel.gtm.constants.js';\n\nexport function getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrlInput, errorLoggingEndpoint) {\n let jsLibUrl = null;\n if(jsLibSource === constants.config.unpkgHostedValue) {\n jsLibUrl = \"https://unpkg.com/@compilacion/colleciones-clientos@latest/dist/browser.gtm.min.js\";\n } else if (jsLibSource === constants.config.selfHostedValue) {\n jsLibUrl = jsLibUrlInput\n }\n let returnObject = {};\n returnObject[constants.config.typeName] = constants.typeCastGtmVariableConfig;\n returnObject[constants.config.emitterEndpoint] = endpoint;\n returnObject[constants.config.flushInterval] = flushInterval;\n returnObject[constants.config.flushSize] = flushSize;\n returnObject[constants.config.trackerName] = trackerName;\n returnObject[constants.config.appName] = appName;\n returnObject[constants.config.sourceName] = jsLibSource;\n returnObject[constants.config.jsLibUrlVariable] = jsLibUrlInput;\n // Optional: providing the URL of the server-side error-logging endpoint is what\n // turns frontend error forwarding on. When left empty the runtime stays silent.\n returnObject[constants.config.errorLoggingEndpoint] = errorLoggingEndpoint;\n return returnObject;\n}\n/* return getConfigObject(endpoint, flushInterval, flushSize, trackerName, appName, jsLibSource, jsLibUrl, errorLoggingEndpoint);*/"],"names":[],"mappings":";;;IAAA,IAAI,SAAS,GAAG;IAChB,IAII,QAAQ,EAAE;IACd,QAAQ,YAAY,EAAE,aAAa;IACnC,QAAQ,kBAAkB,EAAE,2BAA2B;IACvD,QAEQ,eAAe,EAAE,eAAe;IACxC,QAAQ,WAAW,EAAE,WAAW;IAChC,QAAQ,aAAa,EAAE,aAAa;IACpC,QAAQ,SAAS,EAAE,SAAS;IAC5B,QAAQ,iBAAiB,EAAE,iBAAiB;IAC5C,QAAQ,sBAAsB,EAAE,sBAAsB;IACtD,QAAQ,UAAU,EAAE;IACpB,KAAK;IACL,IACI,2BAA2B,EAAE;IACjC,CAAC;;IClBM,SAAS,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,oBAAoB,EAAE;IAO5I,IAAI,IAAI,YAAY,GAAG,EAAE;IACzB,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,yBAAyB;IACjF,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,QAAQ;IAC7D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,aAAa;IAChE,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS;IACxD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,WAAW;IAC5D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO;IACpD,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,WAAW;IAC3D,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,aAAa;IACnE;IACA;IACA,IAAI,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,oBAAoB;IAC9E,IAAI,OAAO,YAAY;IACvB;IACA;;;;;;;;;;"}
@@ -18,30 +18,6 @@
18
18
  }
19
19
  };
20
20
 
21
- /**
22
- * Collects browser-related context values like screen size, language, etc.
23
- * Returns an empty object if not in browser environment.
24
- * @returns {Object}
25
- */
26
- const getBrowserContext = function() {
27
- if (typeof window === 'undefined' || typeof navigator === 'undefined') {
28
- return {};
29
- }
30
- return {
31
- hasFocus: document.hasFocus(),
32
- language: navigator.language,
33
- platform: navigator.platform,
34
- referrer: document.referrer,
35
- screenHeight: window.screen.height,
36
- screenWidth: window.screen.width,
37
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
38
- url: window.location.href,
39
- userAgent: navigator.userAgent,
40
- viewportHeight: window.innerHeight,
41
- viewportWidth: window.innerWidth,
42
- };
43
- };
44
-
45
21
  const formatToCamelCase = function(input) {
46
22
  return input
47
23
  .replace(/[^a-zA-Z0-9]+/g, ' ')
@@ -259,6 +235,27 @@
259
235
  this.context[context] = value;
260
236
  }
261
237
 
238
+ /**
239
+ * Records a namespaced enrichment under `meta.enrichments`. A producer or
240
+ * processor the event passes through claims its own `name` ("I am X") and may
241
+ * declare what subject the data is `about` ("this tells something about Y"), so a
242
+ * model-aware consumer (e.g. the server) can map it onto entity state. Kept OUT
243
+ * of `context` on purpose: enrichments are transport/environment metadata, not
244
+ * part of the domain-validated event payload.
245
+ * @param {string} name - The enrichment namespace (the source identity).
246
+ * @param {string|string[]} about - The subject(s) the data describes (recommended).
247
+ * @param {*} data - The contributed payload.
248
+ */
249
+ setEnrichment = function(name, about, data) {
250
+ if (typeof name !== 'string') {
251
+ throw new Error('Enrichment name must be a string');
252
+ }
253
+ if (!this.meta.enrichments) {
254
+ this.meta.enrichments = {};
255
+ }
256
+ this.meta.enrichments[name] = { about, data };
257
+ }
258
+
262
259
  /**
263
260
  * Alias for `setReference` for compatibility.
264
261
  * Declares an external entity referenced by this event.
@@ -719,6 +716,44 @@
719
716
  this.emitters = emitters;
720
717
  this.trackerName = trackerName;
721
718
  this.appName = appName;
719
+ this.handlers = [];
720
+ }
721
+
722
+ /**
723
+ * Register an enrichment handler. A handler self-identifies (`name` — "I am X")
724
+ * and may declare what it is `about` (the subject(s) — "this tells about Y"); on
725
+ * track it contributes `collect(event)` into `meta.enrichments[name]`. An optional
726
+ * `appliesTo(event)` lets a handler contribute only for relevant events, and
727
+ * `collect` may return `null`/`undefined` to contribute nothing for this event.
728
+ * Handlers stay model-agnostic — `about` is a plain subject string (or array); a
729
+ * model-aware consumer (the server) does the actual mapping.
730
+ * @param {{name: string, about?: string|string[], collect: function, appliesTo?: function}} handler
731
+ * @returns {CollecionesTracker} this (chainable).
732
+ */
733
+ use(handler) {
734
+ if (!handler || typeof handler.name !== 'string' || typeof handler.collect !== 'function') {
735
+ throw new Error('Handler must have a string `name` and a `collect(event)` function');
736
+ }
737
+ this.handlers.push(handler);
738
+ return this;
739
+ }
740
+
741
+ /** Run every registered handler against the event, writing namespaced enrichments. */
742
+ runHandlers(collecionesEvent) {
743
+ this.handlers.forEach((handler) => {
744
+ try {
745
+ if (typeof handler.appliesTo === 'function' && !handler.appliesTo(collecionesEvent)) {
746
+ return;
747
+ }
748
+ const data = handler.collect(collecionesEvent);
749
+ if (data === undefined || data === null) {
750
+ return; // handler declined to contribute for this event
751
+ }
752
+ collecionesEvent.setEnrichment(handler.name, handler.about, data);
753
+ } catch (error) {
754
+ // A failing handler must never drop the event; skip its enrichment.
755
+ }
756
+ });
722
757
  }
723
758
 
724
759
  /**
@@ -731,7 +766,8 @@
731
766
  throw new Error('Event must be of type CollecionesEvent');
732
767
  }
733
768
  collecionesEvent.setTracker(this.trackerName);
734
- collecionesEvent.setAppName(this.appName);
769
+ collecionesEvent.setAppName(this.appName);
770
+ this.runHandlers(collecionesEvent);
735
771
  this.emitters.forEach(element => {
736
772
  element.track(collecionesEvent, this.trackerName, this.appName);
737
773
  });
@@ -753,31 +789,236 @@
753
789
  }
754
790
 
755
791
  /**
756
- * Web-specific tracker that enriches events with browser context before sending them.
757
- * Extends the base CollecionesTracker.
792
+ * Default browser-context enrichment handler. Contributes the page/browser
793
+ * environment under `meta.enrichments.browserContext`.
794
+ *
795
+ * This is transport/environment metadata — deliberately NOT part of the
796
+ * domain-validated `context`. A model-aware consumer (the server) routes each field
797
+ * onto the matching attribute of one of the declared subjects. Returns an empty
798
+ * payload outside a browser.
799
+ */
800
+ const browserContextHandler = {
801
+ name: 'browserContext',
802
+ about: ['session', 'webpage', 'visitor'],
803
+ collect() {
804
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
805
+ return {};
806
+ }
807
+ return {
808
+ hasFocus: document.hasFocus(),
809
+ language: navigator.language,
810
+ platform: navigator.platform,
811
+ referrer: document.referrer,
812
+ screenHeight: window.screen.height,
813
+ screenWidth: window.screen.width,
814
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
815
+ url: window.location.href,
816
+ userAgent: navigator.userAgent,
817
+ viewportHeight: window.innerHeight,
818
+ viewportWidth: window.innerWidth,
819
+ };
820
+ },
821
+ };
822
+
823
+ /**
824
+ * Marketing / attribution handler. Parses the page URL's query string for the
825
+ * standard UTM parameters and the common ad-network click identifiers, so the
826
+ * model-aware consumer (the server) can attribute the session/visitor.
827
+ *
828
+ * Declares `about: ['session', 'visitor']` — attribution describes entity state,
829
+ * not event payload. Returns `null` when the URL carries no marketing parameters,
830
+ * so the handler adds nothing on the (many) events that have none.
831
+ *
832
+ * Field names are the conventional analytics names the domain model also uses
833
+ * (`utmSource`, `marketingNetwork`, ...) — not domain-model coupling, just the
834
+ * shared vocabulary of web attribution.
835
+ */
836
+
837
+ // UTM query parameter -> conventional camelCase field name.
838
+ const UTM_FIELDS = [
839
+ ['utm_source', 'utmSource'],
840
+ ['utm_medium', 'utmMedium'],
841
+ ['utm_campaign', 'utmCampaign'],
842
+ ['utm_term', 'utmTerm'],
843
+ ['utm_content', 'utmContent'],
844
+ ];
845
+
846
+ // Known ad-network click identifiers, in resolution priority, mapped to their network.
847
+ const CLICK_ID_NETWORKS = [
848
+ ['gclid', 'google'],
849
+ ['wbraid', 'google'],
850
+ ['gbraid', 'google'],
851
+ ['dclid', 'google'],
852
+ ['msclkid', 'microsoft'],
853
+ ['fbclid', 'meta'],
854
+ ['igshid', 'meta'],
855
+ ['ttclid', 'tiktok'],
856
+ ['li_fat_id', 'linkedin'],
857
+ ['twclid', 'twitter'],
858
+ ['yclid', 'yandex'],
859
+ ['epik', 'pinterest'],
860
+ ['scid', 'snapchat'],
861
+ ];
862
+
863
+ const marketingHandler = {
864
+ name: 'marketing',
865
+ about: ['session', 'visitor'],
866
+ collect() {
867
+ if (typeof window === 'undefined' || !window.location) {
868
+ return null;
869
+ }
870
+ const params = new URLSearchParams(window.location.search || '');
871
+ const data = {};
872
+
873
+ UTM_FIELDS.forEach(([param, field]) => {
874
+ const value = params.get(param);
875
+ if (value) {
876
+ data[field] = value;
877
+ }
878
+ });
879
+
880
+ for (const [param, network] of CLICK_ID_NETWORKS) {
881
+ const value = params.get(param);
882
+ if (value) {
883
+ data.marketingNetwork = network;
884
+ data.marketingNetworkClickId = value;
885
+ break;
886
+ }
887
+ }
888
+
889
+ return Object.keys(data).length > 0 ? data : null;
890
+ },
891
+ };
892
+
893
+ /**
894
+ * UA Client Hints handler. Surfaces the low-entropy values from
895
+ * `navigator.userAgentData` (Chromium) — `mobile`, `platform`, `brands` — a more
896
+ * reliable device/browser signal than parsing the user-agent string server-side.
897
+ *
898
+ * Declares `about: ['visitor', 'session']`. Returns `null` where UA-CH is
899
+ * unavailable (Firefox/Safari) — the server then falls back to the user-agent
900
+ * string carried by `browserContext`. Only the synchronous low-entropy values are
901
+ * read; the async `getHighEntropyValues()` is intentionally not awaited.
902
+ */
903
+ const userAgentDataHandler = {
904
+ name: 'userAgentData',
905
+ about: ['visitor', 'session'],
906
+ collect() {
907
+ if (typeof navigator === 'undefined' || !navigator.userAgentData) {
908
+ return null;
909
+ }
910
+ const uaData = navigator.userAgentData;
911
+ const data = {
912
+ mobile: Boolean(uaData.mobile),
913
+ };
914
+ if (uaData.platform) {
915
+ data.platform = uaData.platform;
916
+ }
917
+ if (Array.isArray(uaData.brands) && uaData.brands.length > 0) {
918
+ data.brands = uaData.brands.map((b) => ({ brand: b.brand, version: b.version }));
919
+ }
920
+ return data;
921
+ },
922
+ };
923
+
924
+ /**
925
+ * Consent handler. Surfaces the synchronously-available consent signals: the IAB
926
+ * TCF consent string (from the `euconsent-v2` cookie), the detected CMP provider,
927
+ * and the Global Privacy Control signal. The model-aware consumer (the server)
928
+ * decodes the TCF string into granted categories.
929
+ *
930
+ * Declares `about: ['consent', 'session']`. Returns `null` when no consent signal
931
+ * is present. NOTE: only synchronous sources are read — the async `__tcfapi`
932
+ * in-memory state is intentionally not awaited; the persisted cookie is the
933
+ * source of truth.
934
+ */
935
+
936
+ // Known CMP globals -> provider name.
937
+ const CMP_PROVIDERS = [
938
+ ['Cookiebot', 'cookiebot'],
939
+ ['OneTrust', 'onetrust'],
940
+ ['Optanon', 'onetrust'],
941
+ ['Didomi', 'didomi'],
942
+ ['UC_UI', 'usercentrics'],
943
+ ];
944
+
945
+ function readCookie(name) {
946
+ if (typeof document === 'undefined' || typeof document.cookie !== 'string') {
947
+ return undefined;
948
+ }
949
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
950
+ return match ? decodeURIComponent(match[1]) : undefined;
951
+ }
952
+
953
+ function detectProvider() {
954
+ if (typeof window === 'undefined') {
955
+ return undefined;
956
+ }
957
+ for (const [global, name] of CMP_PROVIDERS) {
958
+ if (window[global]) {
959
+ return name;
960
+ }
961
+ }
962
+ if (typeof window.__tcfapi === 'function') {
963
+ return 'iab-tcf';
964
+ }
965
+ return undefined;
966
+ }
967
+
968
+ const consentHandler = {
969
+ name: 'consent',
970
+ about: ['consent', 'session'],
971
+ collect() {
972
+ if (typeof window === 'undefined') {
973
+ return null;
974
+ }
975
+ const data = {};
976
+
977
+ const tcfConsentString = readCookie('euconsent-v2');
978
+ if (tcfConsentString) {
979
+ data.tcfConsentString = tcfConsentString;
980
+ }
981
+
982
+ const provider = detectProvider();
983
+ if (provider) {
984
+ data.consentProvider = provider;
985
+ }
986
+
987
+ if (typeof navigator !== 'undefined' && typeof navigator.globalPrivacyControl === 'boolean') {
988
+ data.globalPrivacyControl = navigator.globalPrivacyControl;
989
+ }
990
+
991
+ return Object.keys(data).length > 0 ? data : null;
992
+ },
993
+ };
994
+
995
+ /**
996
+ * The default enrichment handlers registered on every web tracker. Each handler is
997
+ * model-agnostic: it self-identifies (`name`), declares what it is `about` (subject
998
+ * strings) and `collect`s its data; a model-aware consumer maps it onto entity state.
999
+ */
1000
+ const defaultWebHandlers = [
1001
+ browserContextHandler,
1002
+ marketingHandler,
1003
+ userAgentDataHandler,
1004
+ consentHandler,
1005
+ ];
1006
+
1007
+ /**
1008
+ * Web-specific tracker: the base tracker plus the default web enrichment handlers
1009
+ * (browser context, marketing/attribution, UA client hints, consent), so every web
1010
+ * event carries the client environment and attribution signals in
1011
+ * `meta.enrichments` — never in the domain-validated `context`.
758
1012
  */
759
1013
  class CollecionesWebTracker extends CollecionesTracker {
760
1014
  /**
761
- * Creates a new instance of CollecionesWebTracker.
762
- * @param {Array} emitters - A list of emitter instances used to send the event.
763
- * @param {string} trackerName - The name of the tracker.
764
- * @param {string} appName - The name of the application generating events.
1015
+ * @param {Array} emitters - Emitter instances used to send events.
1016
+ * @param {string} trackerName - The tracker name.
1017
+ * @param {string} appName - The application generating events.
765
1018
  */
766
1019
  constructor(emitters, trackerName, appName) {
767
1020
  super(emitters, trackerName, appName);
768
- }
769
-
770
- /**
771
- * Tracks an event, enriching it with browser context information.
772
- * @param {CollecionesEvent} collecionesEvent - The event object to track.
773
- * @throws {Error} If the event is not an instance of CollecionesEvent.
774
- */
775
- track(collecionesEvent) {
776
- if (!(collecionesEvent instanceof CollecionesEvent)) {
777
- throw new Error('Event must be of type CollecionesEvent');
778
- }
779
- collecionesEvent.setContext('browserContext', getBrowserContext());
780
- super.track(collecionesEvent);
1021
+ defaultWebHandlers.forEach((handler) => this.use(handler));
781
1022
  }
782
1023
  }
783
1024
 
@@ -791,7 +1032,8 @@
791
1032
  'flushSize': 'flushSize',
792
1033
  'trackerName': 'trackerName',
793
1034
  'appName': 'appName',
794
- 'emitterEndpoint': 'emitterEndpoint'},
1035
+ 'emitterEndpoint': 'emitterEndpoint',
1036
+ 'errorLoggingEndpoint': 'errorLoggingEndpoint'},
795
1037
  'collecionesObject': 'compilacionCollecionesClientosGtm',
796
1038
  'typeCastGtmVariableConfig': 'compilacionCollecionesClientosGtmTypeCastConfig'
797
1039
  };
@@ -808,6 +1050,62 @@
808
1050
  let queueReference = null;
809
1051
  let processedQueueLength = 0;
810
1052
 
1053
+ // Identity/destination details captured from the GTM config so a caught error
1054
+ // can be attributed (tracker/app) and forwarded to the right endpoint.
1055
+ let appName = null;
1056
+ let trackerName = null;
1057
+ let errorLoggingEndpoint = null;
1058
+
1059
+ const safeStringify = (value) => {
1060
+ if (value === undefined) return '';
1061
+ if (typeof value === 'string') return value;
1062
+ try {
1063
+ return JSON.stringify(value);
1064
+ } catch (e) {
1065
+ return String(value);
1066
+ }
1067
+ };
1068
+
1069
+ // Forward a caught client-side error to the configured bad-event endpoint.
1070
+ // The whole point of this is to make the try/catch fallbacks visible server-side,
1071
+ // so it is strictly best-effort and must NEVER throw: a failure to report an
1072
+ // error can not be allowed to break the tracking flow it is meant to protect.
1073
+ // Reporting only happens when an errorLoggingEndpoint URL is configured.
1074
+ const reportError = (error, details = {}) => {
1075
+ if (!errorLoggingEndpoint) return;
1076
+ if (typeof fetch !== 'function') return;
1077
+ try {
1078
+ const payload = {
1079
+ errorName: (error && error.name) ? String(error.name) : 'Error',
1080
+ errorMessage: (error && error.message) ? String(error.message) : String(error),
1081
+ tracker: trackerName != null ? String(trackerName) : '',
1082
+ appName: appName != null ? String(appName) : '',
1083
+ entity: details.entity != null ? String(details.entity) : '',
1084
+ action: details.action != null ? String(details.action) : '',
1085
+ rawEvent: safeStringify(details.rawEvent),
1086
+ context: {
1087
+ // phase identifies which fallback caught the error: init, flush or group.
1088
+ phase: details.phase || '',
1089
+ stack: (error && error.stack) ? String(error.stack) : '',
1090
+ url: (typeof window !== 'undefined' && window.location) ? window.location.href : '',
1091
+ userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : '',
1092
+ language: (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : '',
1093
+ clientTimestamp: new Date().toISOString()
1094
+ }
1095
+ };
1096
+ // keepalive lets a report fired during page unload still have a chance to land.
1097
+ fetch(errorLoggingEndpoint, {
1098
+ method: 'POST',
1099
+ credentials: 'include',
1100
+ keepalive: true,
1101
+ headers: { 'Content-Type': 'application/json' },
1102
+ body: JSON.stringify(payload)
1103
+ }).catch(() => {});
1104
+ } catch (e) {
1105
+ // Swallow: error reporting can never be allowed to surface its own exception.
1106
+ }
1107
+ };
1108
+
811
1109
  let init = () => {
812
1110
  if (initialized) return true;
813
1111
  if (window[constants.sharedWindowObjectNames.configVariable] === undefined) return false;
@@ -815,8 +1113,11 @@
815
1113
  if (config.type === undefined || config.type !== constants.typeCastGtmVariableConfig) return false;
816
1114
  let flushInterval = config[constants.config.flushInterval];
817
1115
  let flushSize = config[constants.config.flushSize];
818
- let trackerName = config[constants.config.trackerName];
819
- let appName = config[constants.config.appName];
1116
+ trackerName = config[constants.config.trackerName];
1117
+ appName = config[constants.config.appName];
1118
+ // Capture the error endpoint as soon as a valid config is available so even an
1119
+ // emitter-init failure below can already be reported.
1120
+ errorLoggingEndpoint = config[constants.config.errorLoggingEndpoint] ?? null;
820
1121
  let emitterEndpoint = config[constants.config.emitterEndpoint] ?? config.endpoint;
821
1122
  try {
822
1123
  emitter = new CollecionesEmitter(emitterEndpoint, flushSize, flushInterval);
@@ -825,6 +1126,7 @@
825
1126
  return true;
826
1127
  } catch (e) {
827
1128
  console.error('Error initializing CollecionesEmitter:', e);
1129
+ reportError(e, { phase: 'init' });
828
1130
  return false;
829
1131
  }
830
1132
  };
@@ -1059,6 +1361,12 @@
1059
1361
  added++;
1060
1362
  } catch (e) {
1061
1363
  console.error('Colleciones: skipped malformed grouped event', e);
1364
+ reportError(e, {
1365
+ phase: 'group',
1366
+ rawEvent: eventArg,
1367
+ entity: eventArg?.entity,
1368
+ action: eventArg?.action
1369
+ });
1062
1370
  }
1063
1371
  });
1064
1372
  if (added === 0) return false;
@@ -1078,7 +1386,7 @@
1078
1386
  return true;
1079
1387
  };
1080
1388
 
1081
- let flush = () => {
1389
+ let flushInternal = () => {
1082
1390
  if (!init()) {
1083
1391
  return;
1084
1392
  }
@@ -1103,10 +1411,32 @@
1103
1411
  track(args);
1104
1412
  } catch (e) {
1105
1413
  console.error('Colleciones: failed to process queued event', e);
1414
+ reportError(e, {
1415
+ phase: 'flush',
1416
+ rawEvent: args,
1417
+ entity: args?.entity,
1418
+ action: args?.action
1419
+ });
1106
1420
  }
1107
1421
  }
1108
1422
  };
1109
1423
 
1424
+ // Overarching safety net. The per-event / per-group catches above exist for
1425
+ // RECOVERY (they keep the loop alive), so they intentionally sit inside the loop.
1426
+ // This wrapper catches everything else our own runtime can throw — init, queue
1427
+ // bookkeeping, or any path we forgot to guard — so nothing fails silently again.
1428
+ // It only fires for genuinely unhandled throws (the inner catches swallow theirs),
1429
+ // so there is no double reporting. Scope is our runtime only: page-level errors
1430
+ // from other scripts are deliberately not captured here.
1431
+ let flush = () => {
1432
+ try {
1433
+ flushInternal();
1434
+ } catch (e) {
1435
+ console.error('Colleciones: unexpected runtime failure', e);
1436
+ reportError(e, { phase: 'runtime' });
1437
+ }
1438
+ };
1439
+
1110
1440
  let ensureQueue = () => {
1111
1441
  let queue = window[constants.sharedWindowObjectNames.queue];
1112
1442
  if (!Array.isArray(queue)) {