@hanzo/event 0.3.41 → 0.3.43

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/react.mjs CHANGED
@@ -247,11 +247,17 @@ var RE_CREDENTIAL_PARAM = new RegExp(
247
247
  function redactCredentialParams(s) {
248
248
  return s.replace(RE_CREDENTIAL_PARAM, (_m, prefix) => prefix + REDACTED);
249
249
  }
250
+ var RE_SECRET43 = /(^|[^A-Za-z0-9_-]|%[0-9A-Fa-f]{2})((?=[A-Za-z0-9_-]{0,42}[a-z])(?=[A-Za-z0-9_-]{0,42}[A-Z])[A-Za-z0-9_-]{43})(?![A-Za-z0-9_-])/g;
250
251
  function redactSecrets(s) {
251
252
  s = redactCredentialParams(s);
252
253
  for (const re of SECRET_PATTERNS) s = s.replace(re, REDACTED);
254
+ s = s.replace(RE_SECRET43, (_m, lead) => lead + REDACTED);
253
255
  return redactPAN(s);
254
256
  }
257
+ function withoutFragment(url) {
258
+ const at = url.indexOf("#");
259
+ return at < 0 ? url : url.slice(0, at);
260
+ }
255
261
  function scrubPII(s) {
256
262
  s = s.replace(RE_EMAIL, EMAIL_MARK);
257
263
  s = s.replace(RE_IPV6, IP_MARK);
@@ -378,9 +384,18 @@ function hzAnonId() {
378
384
  }
379
385
  return id;
380
386
  }
387
+ function hzAnonAdopt(id) {
388
+ hzAnonMemo = id;
389
+ hzAnonWrite(HZ_ANON_KEY, id);
390
+ try {
391
+ var s = hzAnonStore();
392
+ if (s) s.setItem(HZ_ANON_KEY, id);
393
+ } catch (e) {
394
+ }
395
+ }
381
396
 
382
397
  // package.json
383
- var version = "0.3.41";
398
+ var version = "0.3.43";
384
399
 
385
400
  // src/version.ts
386
401
  var VERSION = version;
@@ -700,6 +715,13 @@ function sessionId(now = Date.now()) {
700
715
  s.setItem(KEY.session, JSON.stringify(state));
701
716
  return state.id;
702
717
  }
718
+ function adoptSession(id, now = Date.now()) {
719
+ const s = ls();
720
+ if (s) s.setItem(KEY.session, JSON.stringify({ id, last: now }));
721
+ }
722
+ function adoptAnonId(id) {
723
+ hzAnonAdopt(id);
724
+ }
703
725
  function getFirstTouch() {
704
726
  const s = ls();
705
727
  if (!s) return void 0;
@@ -739,6 +761,76 @@ function mergeCohort(patch) {
739
761
  return next;
740
762
  }
741
763
 
764
+ // src/link.ts
765
+ var LINK_PARAMS = { anon: "hz_aid", session: "hz_sid", firstTouch: "hz_ft" };
766
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
767
+ var isId = (v) => !!v && UUID.test(v);
768
+ function encodeFirstTouch(a) {
769
+ return JSON.stringify([
770
+ a.utm.source,
771
+ a.utm.medium,
772
+ a.utm.campaign,
773
+ a.utm.term,
774
+ a.utm.content,
775
+ a.refCode,
776
+ a.referrer,
777
+ a.channel
778
+ ].map((v) => v ?? ""));
779
+ }
780
+ function decodeFirstTouch(raw) {
781
+ if (!raw || raw.length > 1024) return void 0;
782
+ try {
783
+ const t = JSON.parse(raw);
784
+ if (!Array.isArray(t) || t.length !== 8 || t.some((v) => typeof v !== "string")) return void 0;
785
+ const [source, medium, campaign, term, content, refCode, referrer, channel] = t;
786
+ const u = (v) => v || void 0;
787
+ return {
788
+ utm: { source: u(source), medium: u(medium), campaign: u(campaign), term: u(term), content: u(content) },
789
+ refCode: u(refCode),
790
+ referrer: u(referrer),
791
+ channel: u(channel)
792
+ };
793
+ } catch {
794
+ return void 0;
795
+ }
796
+ }
797
+ function linkUrl(url, state, fromHost, base) {
798
+ let u;
799
+ try {
800
+ u = new URL(url, base);
801
+ } catch {
802
+ return url;
803
+ }
804
+ if (u.protocol !== "https:" && u.protocol !== "http:") return url;
805
+ if (!orgOf(u.hostname) || u.hostname === fromHost) return url;
806
+ const set = (k, v) => v && u.searchParams.set(k, v);
807
+ if (isId(state.anonId)) set(LINK_PARAMS.anon, state.anonId);
808
+ if (isId(state.sessionId)) set(LINK_PARAMS.session, state.sessionId);
809
+ if (state.firstTouch) set(LINK_PARAMS.firstTouch, encodeFirstTouch(state.firstTouch));
810
+ return u.toString();
811
+ }
812
+ function readLink(search) {
813
+ const q = new URLSearchParams(search || "");
814
+ const a = q.get(LINK_PARAMS.anon);
815
+ const s = q.get(LINK_PARAMS.session);
816
+ return {
817
+ anonId: isId(a) ? a : void 0,
818
+ sessionId: isId(s) ? s : void 0,
819
+ firstTouch: decodeFirstTouch(q.get(LINK_PARAMS.firstTouch))
820
+ };
821
+ }
822
+ function stripLink(href) {
823
+ const u = new URL(href);
824
+ let hit = false;
825
+ for (const k of Object.values(LINK_PARAMS)) {
826
+ if (u.searchParams.has(k)) {
827
+ u.searchParams.delete(k);
828
+ hit = true;
829
+ }
830
+ }
831
+ return hit ? u.pathname + u.search + u.hash : void 0;
832
+ }
833
+
742
834
  // src/core.ts
743
835
  var EVENT_PATH = "/v1/event";
744
836
  var DEFAULT_HOST = "https://api.hanzo.ai";
@@ -899,8 +991,19 @@ var Analytics = class {
899
991
  if (this.started || !this.cfg.enabled) return;
900
992
  this.started = true;
901
993
  if (!isBrowser()) return;
994
+ const linked = readLink(window.location.search);
995
+ if (linked.anonId) adoptAnonId(linked.anonId);
996
+ if (linked.sessionId) adoptSession(linked.sessionId);
997
+ const clean = stripLink(window.location.href);
998
+ if (clean !== void 0) {
999
+ try {
1000
+ window.history.replaceState(window.history.state, "", clean);
1001
+ } catch {
1002
+ }
1003
+ }
902
1004
  const parsed = parseAttribution(window.location.search, document.referrer);
903
- this.attribution = hasAttribution(parsed) ? setFirstTouchOnce(parsed) : getFirstTouch() ?? parsed;
1005
+ const first = linked.firstTouch && !getFirstTouch() ? setFirstTouchOnce(linked.firstTouch) : void 0;
1006
+ this.attribution = first ?? (hasAttribution(parsed) ? setFirstTouchOnce(parsed) : getFirstTouch() ?? parsed);
904
1007
  this.cohort = mergeCohort({
905
1008
  channel: this.attribution.channel ?? deriveChannel(this.attribution),
906
1009
  refCode: this.attribution.refCode
@@ -919,6 +1022,27 @@ var Analytics = class {
919
1022
  });
920
1023
  }
921
1024
  }
1025
+ /** link returns `url` carrying this visitor's anonymous id, session and first
1026
+ * touch when the destination is a Hanzo-owned host, so the journey continues
1027
+ * across the hop. Any other URL comes back unchanged. */
1028
+ link(url) {
1029
+ if (!isBrowser()) return url;
1030
+ if (!this.started) this.init();
1031
+ return linkUrl(
1032
+ url,
1033
+ { anonId: anonId(), sessionId: sessionId(), firstTouch: this.attribution },
1034
+ window.location.hostname,
1035
+ window.location.href
1036
+ );
1037
+ }
1038
+ /** authorize decorates an OAuth authorize URL for the redirect to IAM and flushes
1039
+ * what is queued, because the page is about to unload. Assign the result to
1040
+ * `location`. */
1041
+ authorize(url) {
1042
+ const out = this.link(url);
1043
+ this.flush(true);
1044
+ return out;
1045
+ }
922
1046
  /** identify binds the current visitor to a stable person id (post-login). */
923
1047
  identify(personId, traits) {
924
1048
  this.personId = personId;
@@ -1102,9 +1226,9 @@ var Analytics = class {
1102
1226
  ...extra
1103
1227
  };
1104
1228
  const capturePII = this.cfg.capturePII ?? false;
1105
- if (wire.url) wire.url = scrubText(wire.url, capturePII);
1106
- if (wire.path) wire.path = scrubText(wire.path, capturePII);
1107
- if (wire.referrer) wire.referrer = scrubText(wire.referrer, capturePII);
1229
+ if (wire.url) wire.url = scrubText(withoutFragment(wire.url), capturePII);
1230
+ if (wire.path) wire.path = scrubText(withoutFragment(wire.path), capturePII);
1231
+ if (wire.referrer) wire.referrer = scrubText(withoutFragment(wire.referrer), capturePII);
1108
1232
  return wire;
1109
1233
  }
1110
1234
  schedule() {