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