@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/link.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Attribution } from './types';
2
+ export declare const LINK_PARAMS: {
3
+ readonly anon: 'hz_aid';
4
+ readonly session: 'hz_sid';
5
+ readonly firstTouch: 'hz_ft';
6
+ };
7
+ export declare const isId: (v: string | null | undefined) => v is string;
8
+ /** What a link carries. Any field may be absent. */
9
+ export interface LinkState {
10
+ anonId?: string;
11
+ sessionId?: string;
12
+ firstTouch?: Attribution;
13
+ }
14
+ /** First touch as a compact tuple: utm source, medium, campaign, term, content, refCode,
15
+ * referrer, channel. Short because it rides a URL; a tuple because keys cost bytes. */
16
+ export declare function encodeFirstTouch(a: Attribution): string;
17
+ export declare function decodeFirstTouch(raw: string | null | undefined): Attribution | undefined;
18
+ /** linkUrl returns `url` with the visitor's identity appended when its host is
19
+ * Hanzo-owned and is not `fromHost`; otherwise `url` unchanged. Relative URLs
20
+ * resolve against `base`. Never throws. */
21
+ export declare function linkUrl(url: string, state: LinkState, fromHost: string, base?: string): string;
22
+ /** readLink parses what a link carried out of a location.search value. Invalid
23
+ * values are dropped, not repaired. */
24
+ export declare function readLink(search: string): LinkState;
25
+ /** stripLink returns the path, query and hash of `href` without the link parameters, or
26
+ * undefined when there were none. */
27
+ export declare function stripLink(href: string): string | undefined;
28
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,eAAO,MAAM,WAAW;aAAK,IAAI,EAAE,QAAQ;aAAE,OAAO,EAAE,QAAQ;aAAE,UAAU,EAAE,OAAO;CAAW,CAAA;AAK9F,eAAO,MAAM,IAAI,MAAO,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,CAAC,IAAI,MAA6B,CAAA;AAEtF,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,WAAW,CAAA;CACzB;AAED;wFACwF;AACxF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAKvD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAgBxF;AAED;;4CAE4C;AAC5C,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAc9F;AAED;wCACwC;AACxC,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CASlD;AAED;sCACsC;AACtC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU1D"}
package/dist/react.cjs CHANGED
@@ -249,11 +249,17 @@ var RE_CREDENTIAL_PARAM = new RegExp(
249
249
  function redactCredentialParams(s) {
250
250
  return s.replace(RE_CREDENTIAL_PARAM, (_m, prefix) => prefix + REDACTED);
251
251
  }
252
+ 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;
252
253
  function redactSecrets(s) {
253
254
  s = redactCredentialParams(s);
254
255
  for (const re of SECRET_PATTERNS) s = s.replace(re, REDACTED);
256
+ s = s.replace(RE_SECRET43, (_m, lead) => lead + REDACTED);
255
257
  return redactPAN(s);
256
258
  }
259
+ function withoutFragment(url) {
260
+ const at = url.indexOf("#");
261
+ return at < 0 ? url : url.slice(0, at);
262
+ }
257
263
  function scrubPII(s) {
258
264
  s = s.replace(RE_EMAIL, EMAIL_MARK);
259
265
  s = s.replace(RE_IPV6, IP_MARK);
@@ -380,9 +386,18 @@ function hzAnonId() {
380
386
  }
381
387
  return id;
382
388
  }
389
+ function hzAnonAdopt(id) {
390
+ hzAnonMemo = id;
391
+ hzAnonWrite(HZ_ANON_KEY, id);
392
+ try {
393
+ var s = hzAnonStore();
394
+ if (s) s.setItem(HZ_ANON_KEY, id);
395
+ } catch (e) {
396
+ }
397
+ }
383
398
 
384
399
  // package.json
385
- var version = "0.3.41";
400
+ var version = "0.3.43";
386
401
 
387
402
  // src/version.ts
388
403
  var VERSION = version;
@@ -702,6 +717,13 @@ function sessionId(now = Date.now()) {
702
717
  s.setItem(KEY.session, JSON.stringify(state));
703
718
  return state.id;
704
719
  }
720
+ function adoptSession(id, now = Date.now()) {
721
+ const s = ls();
722
+ if (s) s.setItem(KEY.session, JSON.stringify({ id, last: now }));
723
+ }
724
+ function adoptAnonId(id) {
725
+ hzAnonAdopt(id);
726
+ }
705
727
  function getFirstTouch() {
706
728
  const s = ls();
707
729
  if (!s) return void 0;
@@ -741,6 +763,76 @@ function mergeCohort(patch) {
741
763
  return next;
742
764
  }
743
765
 
766
+ // src/link.ts
767
+ var LINK_PARAMS = { anon: "hz_aid", session: "hz_sid", firstTouch: "hz_ft" };
768
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
769
+ var isId = (v) => !!v && UUID.test(v);
770
+ function encodeFirstTouch(a) {
771
+ return JSON.stringify([
772
+ a.utm.source,
773
+ a.utm.medium,
774
+ a.utm.campaign,
775
+ a.utm.term,
776
+ a.utm.content,
777
+ a.refCode,
778
+ a.referrer,
779
+ a.channel
780
+ ].map((v) => v ?? ""));
781
+ }
782
+ function decodeFirstTouch(raw) {
783
+ if (!raw || raw.length > 1024) return void 0;
784
+ try {
785
+ const t = JSON.parse(raw);
786
+ if (!Array.isArray(t) || t.length !== 8 || t.some((v) => typeof v !== "string")) return void 0;
787
+ const [source, medium, campaign, term, content, refCode, referrer, channel] = t;
788
+ const u = (v) => v || void 0;
789
+ return {
790
+ utm: { source: u(source), medium: u(medium), campaign: u(campaign), term: u(term), content: u(content) },
791
+ refCode: u(refCode),
792
+ referrer: u(referrer),
793
+ channel: u(channel)
794
+ };
795
+ } catch {
796
+ return void 0;
797
+ }
798
+ }
799
+ function linkUrl(url, state, fromHost, base) {
800
+ let u;
801
+ try {
802
+ u = new URL(url, base);
803
+ } catch {
804
+ return url;
805
+ }
806
+ if (u.protocol !== "https:" && u.protocol !== "http:") return url;
807
+ if (!orgOf(u.hostname) || u.hostname === fromHost) return url;
808
+ const set = (k, v) => v && u.searchParams.set(k, v);
809
+ if (isId(state.anonId)) set(LINK_PARAMS.anon, state.anonId);
810
+ if (isId(state.sessionId)) set(LINK_PARAMS.session, state.sessionId);
811
+ if (state.firstTouch) set(LINK_PARAMS.firstTouch, encodeFirstTouch(state.firstTouch));
812
+ return u.toString();
813
+ }
814
+ function readLink(search) {
815
+ const q = new URLSearchParams(search || "");
816
+ const a = q.get(LINK_PARAMS.anon);
817
+ const s = q.get(LINK_PARAMS.session);
818
+ return {
819
+ anonId: isId(a) ? a : void 0,
820
+ sessionId: isId(s) ? s : void 0,
821
+ firstTouch: decodeFirstTouch(q.get(LINK_PARAMS.firstTouch))
822
+ };
823
+ }
824
+ function stripLink(href) {
825
+ const u = new URL(href);
826
+ let hit = false;
827
+ for (const k of Object.values(LINK_PARAMS)) {
828
+ if (u.searchParams.has(k)) {
829
+ u.searchParams.delete(k);
830
+ hit = true;
831
+ }
832
+ }
833
+ return hit ? u.pathname + u.search + u.hash : void 0;
834
+ }
835
+
744
836
  // src/core.ts
745
837
  var EVENT_PATH = "/v1/event";
746
838
  var DEFAULT_HOST = "https://api.hanzo.ai";
@@ -901,8 +993,19 @@ var Analytics = class {
901
993
  if (this.started || !this.cfg.enabled) return;
902
994
  this.started = true;
903
995
  if (!isBrowser()) return;
996
+ const linked = readLink(window.location.search);
997
+ if (linked.anonId) adoptAnonId(linked.anonId);
998
+ if (linked.sessionId) adoptSession(linked.sessionId);
999
+ const clean = stripLink(window.location.href);
1000
+ if (clean !== void 0) {
1001
+ try {
1002
+ window.history.replaceState(window.history.state, "", clean);
1003
+ } catch {
1004
+ }
1005
+ }
904
1006
  const parsed = parseAttribution(window.location.search, document.referrer);
905
- this.attribution = hasAttribution(parsed) ? setFirstTouchOnce(parsed) : getFirstTouch() ?? parsed;
1007
+ const first = linked.firstTouch && !getFirstTouch() ? setFirstTouchOnce(linked.firstTouch) : void 0;
1008
+ this.attribution = first ?? (hasAttribution(parsed) ? setFirstTouchOnce(parsed) : getFirstTouch() ?? parsed);
906
1009
  this.cohort = mergeCohort({
907
1010
  channel: this.attribution.channel ?? deriveChannel(this.attribution),
908
1011
  refCode: this.attribution.refCode
@@ -921,6 +1024,27 @@ var Analytics = class {
921
1024
  });
922
1025
  }
923
1026
  }
1027
+ /** link returns `url` carrying this visitor's anonymous id, session and first
1028
+ * touch when the destination is a Hanzo-owned host, so the journey continues
1029
+ * across the hop. Any other URL comes back unchanged. */
1030
+ link(url) {
1031
+ if (!isBrowser()) return url;
1032
+ if (!this.started) this.init();
1033
+ return linkUrl(
1034
+ url,
1035
+ { anonId: anonId(), sessionId: sessionId(), firstTouch: this.attribution },
1036
+ window.location.hostname,
1037
+ window.location.href
1038
+ );
1039
+ }
1040
+ /** authorize decorates an OAuth authorize URL for the redirect to IAM and flushes
1041
+ * what is queued, because the page is about to unload. Assign the result to
1042
+ * `location`. */
1043
+ authorize(url) {
1044
+ const out = this.link(url);
1045
+ this.flush(true);
1046
+ return out;
1047
+ }
924
1048
  /** identify binds the current visitor to a stable person id (post-login). */
925
1049
  identify(personId, traits) {
926
1050
  this.personId = personId;
@@ -1104,9 +1228,9 @@ var Analytics = class {
1104
1228
  ...extra
1105
1229
  };
1106
1230
  const capturePII = this.cfg.capturePII ?? false;
1107
- if (wire.url) wire.url = scrubText(wire.url, capturePII);
1108
- if (wire.path) wire.path = scrubText(wire.path, capturePII);
1109
- if (wire.referrer) wire.referrer = scrubText(wire.referrer, capturePII);
1231
+ if (wire.url) wire.url = scrubText(withoutFragment(wire.url), capturePII);
1232
+ if (wire.path) wire.path = scrubText(withoutFragment(wire.path), capturePII);
1233
+ if (wire.referrer) wire.referrer = scrubText(withoutFragment(wire.referrer), capturePII);
1110
1234
  return wire;
1111
1235
  }
1112
1236
  schedule() {