@compilacion/colleciones-clientos 2.0.30 → 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.
@@ -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