@compilacion/colleciones-clientos 2.0.30 → 2.0.33

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.
@@ -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.
@@ -626,6 +623,44 @@
626
623
  this.emitters = emitters;
627
624
  this.trackerName = trackerName;
628
625
  this.appName = appName;
626
+ this.handlers = [];
627
+ }
628
+
629
+ /**
630
+ * Register an enrichment handler. A handler self-identifies (`name` — "I am X")
631
+ * and may declare what it is `about` (the subject(s) — "this tells about Y"); on
632
+ * track it contributes `collect(event)` into `meta.enrichments[name]`. An optional
633
+ * `appliesTo(event)` lets a handler contribute only for relevant events, and
634
+ * `collect` may return `null`/`undefined` to contribute nothing for this event.
635
+ * Handlers stay model-agnostic — `about` is a plain subject string (or array); a
636
+ * model-aware consumer (the server) does the actual mapping.
637
+ * @param {{name: string, about?: string|string[], collect: function, appliesTo?: function}} handler
638
+ * @returns {CollecionesTracker} this (chainable).
639
+ */
640
+ use(handler) {
641
+ if (!handler || typeof handler.name !== 'string' || typeof handler.collect !== 'function') {
642
+ throw new Error('Handler must have a string `name` and a `collect(event)` function');
643
+ }
644
+ this.handlers.push(handler);
645
+ return this;
646
+ }
647
+
648
+ /** Run every registered handler against the event, writing namespaced enrichments. */
649
+ runHandlers(collecionesEvent) {
650
+ this.handlers.forEach((handler) => {
651
+ try {
652
+ if (typeof handler.appliesTo === 'function' && !handler.appliesTo(collecionesEvent)) {
653
+ return;
654
+ }
655
+ const data = handler.collect(collecionesEvent);
656
+ if (data === undefined || data === null) {
657
+ return; // handler declined to contribute for this event
658
+ }
659
+ collecionesEvent.setEnrichment(handler.name, handler.about, data);
660
+ } catch (error) {
661
+ // A failing handler must never drop the event; skip its enrichment.
662
+ }
663
+ });
629
664
  }
630
665
 
631
666
  /**
@@ -638,7 +673,8 @@
638
673
  throw new Error('Event must be of type CollecionesEvent');
639
674
  }
640
675
  collecionesEvent.setTracker(this.trackerName);
641
- collecionesEvent.setAppName(this.appName);
676
+ collecionesEvent.setAppName(this.appName);
677
+ this.runHandlers(collecionesEvent);
642
678
  this.emitters.forEach(element => {
643
679
  element.track(collecionesEvent, this.trackerName, this.appName);
644
680
  });
@@ -660,31 +696,236 @@
660
696
  }
661
697
 
662
698
  /**
663
- * Web-specific tracker that enriches events with browser context before sending them.
664
- * Extends the base CollecionesTracker.
699
+ * Default browser-context enrichment handler. Contributes the page/browser
700
+ * environment under `meta.enrichments.browserContext`.
701
+ *
702
+ * This is transport/environment metadata — deliberately NOT part of the
703
+ * domain-validated `context`. A model-aware consumer (the server) routes each field
704
+ * onto the matching attribute of one of the declared subjects. Returns an empty
705
+ * payload outside a browser.
706
+ */
707
+ const browserContextHandler = {
708
+ name: 'browserContext',
709
+ about: ['session', 'webpage'],
710
+ collect() {
711
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
712
+ return {};
713
+ }
714
+ return {
715
+ hasFocus: document.hasFocus(),
716
+ language: navigator.language,
717
+ platform: navigator.platform,
718
+ referrer: document.referrer,
719
+ screenHeight: window.screen.height,
720
+ screenWidth: window.screen.width,
721
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
722
+ url: window.location.href,
723
+ userAgent: navigator.userAgent,
724
+ viewportHeight: window.innerHeight,
725
+ viewportWidth: window.innerWidth,
726
+ };
727
+ },
728
+ };
729
+
730
+ /**
731
+ * Marketing / attribution handler. Parses the page URL's query string for the
732
+ * standard UTM parameters and the common ad-network click identifiers, so the
733
+ * model-aware consumer (the server) can attribute the session/visitor.
734
+ *
735
+ * Declares `about: ['session', 'visitor']` — attribution describes entity state,
736
+ * not event payload. Returns `null` when the URL carries no marketing parameters,
737
+ * so the handler adds nothing on the (many) events that have none.
738
+ *
739
+ * Field names are the conventional analytics names the domain model also uses
740
+ * (`utmSource`, `marketingNetwork`, ...) — not domain-model coupling, just the
741
+ * shared vocabulary of web attribution.
742
+ */
743
+
744
+ // UTM query parameter -> conventional camelCase field name.
745
+ const UTM_FIELDS = [
746
+ ['utm_source', 'utmSource'],
747
+ ['utm_medium', 'utmMedium'],
748
+ ['utm_campaign', 'utmCampaign'],
749
+ ['utm_term', 'utmTerm'],
750
+ ['utm_content', 'utmContent'],
751
+ ];
752
+
753
+ // Known ad-network click identifiers, in resolution priority, mapped to their network.
754
+ const CLICK_ID_NETWORKS = [
755
+ ['gclid', 'google'],
756
+ ['wbraid', 'google'],
757
+ ['gbraid', 'google'],
758
+ ['dclid', 'google'],
759
+ ['msclkid', 'microsoft'],
760
+ ['fbclid', 'meta'],
761
+ ['igshid', 'meta'],
762
+ ['ttclid', 'tiktok'],
763
+ ['li_fat_id', 'linkedin'],
764
+ ['twclid', 'twitter'],
765
+ ['yclid', 'yandex'],
766
+ ['epik', 'pinterest'],
767
+ ['scid', 'snapchat'],
768
+ ];
769
+
770
+ const marketingHandler = {
771
+ name: 'marketing',
772
+ about: ['session', 'webpage'],
773
+ collect() {
774
+ if (typeof window === 'undefined' || !window.location) {
775
+ return null;
776
+ }
777
+ const params = new URLSearchParams(window.location.search || '');
778
+ const data = {};
779
+
780
+ UTM_FIELDS.forEach(([param, field]) => {
781
+ const value = params.get(param);
782
+ if (value) {
783
+ data[field] = value;
784
+ }
785
+ });
786
+
787
+ for (const [param, network] of CLICK_ID_NETWORKS) {
788
+ const value = params.get(param);
789
+ if (value) {
790
+ data.marketingNetwork = network;
791
+ data.marketingNetworkClickId = value;
792
+ break;
793
+ }
794
+ }
795
+
796
+ return Object.keys(data).length > 0 ? data : null;
797
+ },
798
+ };
799
+
800
+ /**
801
+ * UA Client Hints handler. Surfaces the low-entropy values from
802
+ * `navigator.userAgentData` (Chromium) — `mobile`, `platform`, `brands` — a more
803
+ * reliable device/browser signal than parsing the user-agent string server-side.
804
+ *
805
+ * Declares `about: ['visitor', 'session']`. Returns `null` where UA-CH is
806
+ * unavailable (Firefox/Safari) — the server then falls back to the user-agent
807
+ * string carried by `browserContext`. Only the synchronous low-entropy values are
808
+ * read; the async `getHighEntropyValues()` is intentionally not awaited.
809
+ */
810
+ const userAgentDataHandler = {
811
+ name: 'userAgentData',
812
+ about: ['session', 'webpage'],
813
+ collect() {
814
+ if (typeof navigator === 'undefined' || !navigator.userAgentData) {
815
+ return null;
816
+ }
817
+ const uaData = navigator.userAgentData;
818
+ const data = {
819
+ mobile: Boolean(uaData.mobile),
820
+ };
821
+ if (uaData.platform) {
822
+ data.platform = uaData.platform;
823
+ }
824
+ if (Array.isArray(uaData.brands) && uaData.brands.length > 0) {
825
+ data.brands = uaData.brands.map((b) => ({ brand: b.brand, version: b.version }));
826
+ }
827
+ return data;
828
+ },
829
+ };
830
+
831
+ /**
832
+ * Consent handler. Surfaces the synchronously-available consent signals: the IAB
833
+ * TCF consent string (from the `euconsent-v2` cookie), the detected CMP provider,
834
+ * and the Global Privacy Control signal. The model-aware consumer (the server)
835
+ * decodes the TCF string into granted categories.
836
+ *
837
+ * Declares `about: ['consent', 'session']`. Returns `null` when no consent signal
838
+ * is present. NOTE: only synchronous sources are read — the async `__tcfapi`
839
+ * in-memory state is intentionally not awaited; the persisted cookie is the
840
+ * source of truth.
841
+ */
842
+
843
+ // Known CMP globals -> provider name.
844
+ const CMP_PROVIDERS = [
845
+ ['Cookiebot', 'cookiebot'],
846
+ ['OneTrust', 'onetrust'],
847
+ ['Optanon', 'onetrust'],
848
+ ['Didomi', 'didomi'],
849
+ ['UC_UI', 'usercentrics'],
850
+ ];
851
+
852
+ function readCookie(name) {
853
+ if (typeof document === 'undefined' || typeof document.cookie !== 'string') {
854
+ return undefined;
855
+ }
856
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
857
+ return match ? decodeURIComponent(match[1]) : undefined;
858
+ }
859
+
860
+ function detectProvider() {
861
+ if (typeof window === 'undefined') {
862
+ return undefined;
863
+ }
864
+ for (const [global, name] of CMP_PROVIDERS) {
865
+ if (window[global]) {
866
+ return name;
867
+ }
868
+ }
869
+ if (typeof window.__tcfapi === 'function') {
870
+ return 'iab-tcf';
871
+ }
872
+ return undefined;
873
+ }
874
+
875
+ const consentHandler = {
876
+ name: 'consent',
877
+ about: ['consent', 'session', 'visitor'],
878
+ collect() {
879
+ if (typeof window === 'undefined') {
880
+ return null;
881
+ }
882
+ const data = {};
883
+
884
+ const tcfConsentString = readCookie('euconsent-v2');
885
+ if (tcfConsentString) {
886
+ data.tcfConsentString = tcfConsentString;
887
+ }
888
+
889
+ const provider = detectProvider();
890
+ if (provider) {
891
+ data.consentProvider = provider;
892
+ }
893
+
894
+ if (typeof navigator !== 'undefined' && typeof navigator.globalPrivacyControl === 'boolean') {
895
+ data.globalPrivacyControl = navigator.globalPrivacyControl;
896
+ }
897
+
898
+ return Object.keys(data).length > 0 ? data : null;
899
+ },
900
+ };
901
+
902
+ /**
903
+ * The default enrichment handlers registered on every web tracker. Each handler is
904
+ * model-agnostic: it self-identifies (`name`), declares what it is `about` (subject
905
+ * strings) and `collect`s its data; a model-aware consumer maps it onto entity state.
906
+ */
907
+ const defaultWebHandlers = [
908
+ browserContextHandler,
909
+ marketingHandler,
910
+ userAgentDataHandler,
911
+ consentHandler,
912
+ ];
913
+
914
+ /**
915
+ * Web-specific tracker: the base tracker plus the default web enrichment handlers
916
+ * (browser context, marketing/attribution, UA client hints, consent), so every web
917
+ * event carries the client environment and attribution signals in
918
+ * `meta.enrichments` — never in the domain-validated `context`.
665
919
  */
666
920
  class CollecionesWebTracker extends CollecionesTracker {
667
921
  /**
668
- * Creates a new instance of CollecionesWebTracker.
669
- * @param {Array} emitters - A list of emitter instances used to send the event.
670
- * @param {string} trackerName - The name of the tracker.
671
- * @param {string} appName - The name of the application generating events.
922
+ * @param {Array} emitters - Emitter instances used to send events.
923
+ * @param {string} trackerName - The tracker name.
924
+ * @param {string} appName - The application generating events.
672
925
  */
673
926
  constructor(emitters, trackerName, appName) {
674
927
  super(emitters, trackerName, appName);
675
- }
676
-
677
- /**
678
- * Tracks an event, enriching it with browser context information.
679
- * @param {CollecionesEvent} collecionesEvent - The event object to track.
680
- * @throws {Error} If the event is not an instance of CollecionesEvent.
681
- */
682
- track(collecionesEvent) {
683
- if (!(collecionesEvent instanceof CollecionesEvent)) {
684
- throw new Error('Event must be of type CollecionesEvent');
685
- }
686
- collecionesEvent.setContext('browserContext', getBrowserContext());
687
- super.track(collecionesEvent);
928
+ defaultWebHandlers.forEach((handler) => this.use(handler));
688
929
  }
689
930
  }
690
931