@hanzo/event 0.3.42 → 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
@@ -386,9 +386,18 @@ function hzAnonId() {
386
386
  }
387
387
  return id;
388
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
+ }
389
398
 
390
399
  // package.json
391
- var version = "0.3.42";
400
+ var version = "0.3.43";
392
401
 
393
402
  // src/version.ts
394
403
  var VERSION = version;
@@ -708,6 +717,13 @@ function sessionId(now = Date.now()) {
708
717
  s.setItem(KEY.session, JSON.stringify(state));
709
718
  return state.id;
710
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
+ }
711
727
  function getFirstTouch() {
712
728
  const s = ls();
713
729
  if (!s) return void 0;
@@ -747,6 +763,76 @@ function mergeCohort(patch) {
747
763
  return next;
748
764
  }
749
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
+
750
836
  // src/core.ts
751
837
  var EVENT_PATH = "/v1/event";
752
838
  var DEFAULT_HOST = "https://api.hanzo.ai";
@@ -907,8 +993,19 @@ var Analytics = class {
907
993
  if (this.started || !this.cfg.enabled) return;
908
994
  this.started = true;
909
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
+ }
910
1006
  const parsed = parseAttribution(window.location.search, document.referrer);
911
- 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);
912
1009
  this.cohort = mergeCohort({
913
1010
  channel: this.attribution.channel ?? deriveChannel(this.attribution),
914
1011
  refCode: this.attribution.refCode
@@ -927,6 +1024,27 @@ var Analytics = class {
927
1024
  });
928
1025
  }
929
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
+ }
930
1048
  /** identify binds the current visitor to a stable person id (post-login). */
931
1049
  identify(personId, traits) {
932
1050
  this.personId = personId;