@hanzo/event 0.3.25 → 0.3.27

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 hanzo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -285,11 +285,19 @@ declare class Analytics {
285
285
  private attribution;
286
286
  private cohort;
287
287
  private started;
288
+ /** The view pageview() last counted — path + location. */
289
+ private counted?;
288
290
  /** Parsed error-plane DSN, or null when the plane is inert. */
289
291
  private dsn;
290
292
  /** Guards against an error thrown *inside* the error path re-entering it. */
291
293
  private reentrant;
292
294
  constructor(config: AnalyticsConfig);
295
+ /** adopt gives this client a credential it does not have. The key belongs to
296
+ * the stream, not to whichever caller happened to ask for the handle first,
297
+ * so a later caller carrying one hands it over. The error plane derives from
298
+ * the key, so it comes up here too when it was inert for want of one.
299
+ * Present fields are never overwritten: the first caller's key stays. */
300
+ adopt(config: AnalyticsConfig): void;
293
301
  /** errorPlaneEnabled reports whether captured exceptions can actually reach the
294
302
  * error host. False means a DSN was never configured — the documented
295
303
  * fail-safe. Exposed so an app (or a test) can assert its wiring instead of
@@ -308,7 +316,9 @@ declare class Analytics {
308
316
  /** group associates the visitor with an org/team (analytics grouping, not the
309
317
  * server tenant — the server still derives tenant from the session). */
310
318
  group(groupId: string, traits?: Record<string, unknown>): void;
311
- /** pageview records a $pageview for the current (or given) location. */
319
+ /** pageview records a $pageview for the current (or given) location, once per
320
+ * view. A view is the path plus the full location, so a query or hash change
321
+ * is a new one and a repeat call for the same place is not. */
312
322
  pageview(path?: string, properties?: Record<string, unknown>): void;
313
323
  /** capture records a named product event with optional properties. Commerce
314
324
  * fields (productId/quantity/revenue/currency) may be passed for order events. */
@@ -339,8 +349,8 @@ declare class Analytics {
339
349
  * front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
340
350
  * unload-safe transport. Auth is orthogonal to the wire:
341
351
  *
342
- * • publishable key set → rides Authorization: Bearer pk_… (fetch) or
343
- * ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
352
+ * • publishable key set → rides ?ingest_key=pk-… on both sends, keeping each
353
+ * a CORS-simple request that any origin may send.
344
354
  * • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
345
355
  * carry a header, so token apps fall back to keepalive fetch on unload).
346
356
  * • else a cookie app rides same-origin credentials (beacon carries the
@@ -362,7 +372,9 @@ declare class Analytics {
362
372
  private schedule;
363
373
  private clearTimer;
364
374
  }
365
- /** createAnalytics builds a client instance. Most apps use one shared instance. */
375
+ /** createAnalytics returns the client for a stream, building it on first ask.
376
+ * This is the ONE way to get a client: `new Analytics` bypasses the registry
377
+ * and is for tests and for a deliberately separate instance. */
366
378
  declare function createAnalytics(config: AnalyticsConfig): Analytics;
367
379
 
368
380
  export { type Attribution as A, type Cohort as C, type Dsn as D, type ExceptionEntry as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, type SentryLevel as c, type ExceptionProperties as d, Analytics as e, type AnalyticsConfig as f, type EventKind as g, type Exception as h, type ExceptionFrame as i, createAnalytics as j };
@@ -285,11 +285,19 @@ declare class Analytics {
285
285
  private attribution;
286
286
  private cohort;
287
287
  private started;
288
+ /** The view pageview() last counted — path + location. */
289
+ private counted?;
288
290
  /** Parsed error-plane DSN, or null when the plane is inert. */
289
291
  private dsn;
290
292
  /** Guards against an error thrown *inside* the error path re-entering it. */
291
293
  private reentrant;
292
294
  constructor(config: AnalyticsConfig);
295
+ /** adopt gives this client a credential it does not have. The key belongs to
296
+ * the stream, not to whichever caller happened to ask for the handle first,
297
+ * so a later caller carrying one hands it over. The error plane derives from
298
+ * the key, so it comes up here too when it was inert for want of one.
299
+ * Present fields are never overwritten: the first caller's key stays. */
300
+ adopt(config: AnalyticsConfig): void;
293
301
  /** errorPlaneEnabled reports whether captured exceptions can actually reach the
294
302
  * error host. False means a DSN was never configured — the documented
295
303
  * fail-safe. Exposed so an app (or a test) can assert its wiring instead of
@@ -308,7 +316,9 @@ declare class Analytics {
308
316
  /** group associates the visitor with an org/team (analytics grouping, not the
309
317
  * server tenant — the server still derives tenant from the session). */
310
318
  group(groupId: string, traits?: Record<string, unknown>): void;
311
- /** pageview records a $pageview for the current (or given) location. */
319
+ /** pageview records a $pageview for the current (or given) location, once per
320
+ * view. A view is the path plus the full location, so a query or hash change
321
+ * is a new one and a repeat call for the same place is not. */
312
322
  pageview(path?: string, properties?: Record<string, unknown>): void;
313
323
  /** capture records a named product event with optional properties. Commerce
314
324
  * fields (productId/quantity/revenue/currency) may be passed for order events. */
@@ -339,8 +349,8 @@ declare class Analytics {
339
349
  * front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
340
350
  * unload-safe transport. Auth is orthogonal to the wire:
341
351
  *
342
- * • publishable key set → rides Authorization: Bearer pk_… (fetch) or
343
- * ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
352
+ * • publishable key set → rides ?ingest_key=pk-… on both sends, keeping each
353
+ * a CORS-simple request that any origin may send.
344
354
  * • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
345
355
  * carry a header, so token apps fall back to keepalive fetch on unload).
346
356
  * • else a cookie app rides same-origin credentials (beacon carries the
@@ -362,7 +372,9 @@ declare class Analytics {
362
372
  private schedule;
363
373
  private clearTimer;
364
374
  }
365
- /** createAnalytics builds a client instance. Most apps use one shared instance. */
375
+ /** createAnalytics returns the client for a stream, building it on first ask.
376
+ * This is the ONE way to get a client: `new Analytics` bypasses the registry
377
+ * and is for tests and for a deliberately separate instance. */
366
378
  declare function createAnalytics(config: AnalyticsConfig): Analytics;
367
379
 
368
380
  export { type Attribution as A, type Cohort as C, type Dsn as D, type ExceptionEntry as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, type SentryLevel as c, type ExceptionProperties as d, Analytics as e, type AnalyticsConfig as f, type EventKind as g, type Exception as h, type ExceptionFrame as i, createAnalytics as j };
package/dist/index.cjs CHANGED
@@ -319,7 +319,7 @@ function uuidv7Time(id) {
319
319
  }
320
320
 
321
321
  // src/version.ts
322
- var VERSION = "0.3.25";
322
+ var VERSION = "0.3.27";
323
323
 
324
324
  // src/sentry.ts
325
325
  var MAX_FRAMES = 50;
@@ -736,17 +736,18 @@ var DefaultTransport = class {
736
736
  }
737
737
  }
738
738
  if (typeof fetch !== "function") return;
739
+ const simple = opts.ingestKey !== void 0 && opts.contentType === void 0;
739
740
  const headers = {
740
- "Content-Type": opts.contentType ?? "application/json"
741
+ "Content-Type": simple ? BEACON_CONTENT_TYPE : opts.contentType ?? "application/json"
741
742
  };
742
- const bearer = opts.ingestKey ?? opts.token;
743
+ const bearer = simple ? void 0 : opts.ingestKey ?? opts.token;
743
744
  if (bearer) headers.Authorization = `Bearer ${bearer}`;
744
- void fetch(url, {
745
+ void fetch(simple ? appendQuery(url, "ingest_key", opts.ingestKey) : url, {
745
746
  method: "POST",
746
747
  headers,
747
748
  body,
748
749
  keepalive: true,
749
- credentials: "include"
750
+ credentials: simple ? "omit" : "include"
750
751
  }).then((res) => {
751
752
  if (!res.ok && opts.debug) {
752
753
  console.warn("[event] ingest rejected", res.status, url.split("?")[0]);
@@ -795,6 +796,20 @@ var Analytics = class {
795
796
  config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product, this.cfg.ingestKey)
796
797
  );
797
798
  }
799
+ /** adopt gives this client a credential it does not have. The key belongs to
800
+ * the stream, not to whichever caller happened to ask for the handle first,
801
+ * so a later caller carrying one hands it over. The error plane derives from
802
+ * the key, so it comes up here too when it was inert for want of one.
803
+ * Present fields are never overwritten: the first caller's key stays. */
804
+ adopt(config) {
805
+ if (config.ingestKey && !this.cfg.ingestKey) this.cfg.ingestKey = config.ingestKey;
806
+ if (config.getToken && !this.cfg.getToken) this.cfg.getToken = config.getToken;
807
+ if (!this.dsn) {
808
+ this.dsn = parseDsn(
809
+ config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product, this.cfg.ingestKey)
810
+ );
811
+ }
812
+ }
798
813
  /** errorPlaneEnabled reports whether captured exceptions can actually reach the
799
814
  * error host. False means a DSN was never configured — the documented
800
815
  * fail-safe. Exposed so an app (or a test) can assert its wiring instead of
@@ -845,9 +860,14 @@ var Analytics = class {
845
860
  group(groupId, traits) {
846
861
  this.enqueue("group", void 0, { groupId, properties: traits });
847
862
  }
848
- /** pageview records a $pageview for the current (or given) location. */
863
+ /** pageview records a $pageview for the current (or given) location, once per
864
+ * view. A view is the path plus the full location, so a query or hash change
865
+ * is a new one and a repeat call for the same place is not. */
849
866
  pageview(path, properties) {
850
867
  const p = path ?? (isBrowser() ? window.location.pathname : void 0);
868
+ const view = (p ?? "") + "\0" + (isBrowser() ? window.location.href : "");
869
+ if (view === this.counted) return;
870
+ this.counted = view;
851
871
  this.enqueue("pageview", events.PAGEVIEW, { path: p, properties });
852
872
  }
853
873
  /** capture records a named product event with optional properties. Commerce
@@ -908,8 +928,8 @@ var Analytics = class {
908
928
  * front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
909
929
  * unload-safe transport. Auth is orthogonal to the wire:
910
930
  *
911
- * • publishable key set → rides Authorization: Bearer pk_… (fetch) or
912
- * ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
931
+ * • publishable key set → rides ?ingest_key=pk-… on both sends, keeping each
932
+ * a CORS-simple request that any origin may send.
913
933
  * • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
914
934
  * carry a header, so token apps fall back to keepalive fetch on unload).
915
935
  * • else a cookie app rides same-origin credentials (beacon carries the
@@ -1032,8 +1052,27 @@ var Analytics = class {
1032
1052
  }
1033
1053
  }
1034
1054
  };
1055
+ var CLIENTS = /* @__PURE__ */ Symbol.for("hanzo.event.clients");
1056
+ function registry() {
1057
+ const g = globalThis;
1058
+ const existing = g[CLIENTS];
1059
+ if (existing) return existing;
1060
+ const fresh = /* @__PURE__ */ new Map();
1061
+ g[CLIENTS] = fresh;
1062
+ return fresh;
1063
+ }
1035
1064
  function createAnalytics(config) {
1036
- return new Analytics(config);
1065
+ if (!isBrowser()) return new Analytics(config);
1066
+ const key = (config.host ?? DEFAULT_HOST) + "\0" + config.product;
1067
+ const clients = registry();
1068
+ const existing = clients.get(key);
1069
+ if (existing) {
1070
+ existing.adopt(config);
1071
+ return existing;
1072
+ }
1073
+ const fresh = new Analytics(config);
1074
+ clients.set(key, fresh);
1075
+ return fresh;
1037
1076
  }
1038
1077
 
1039
1078
  // src/goals.ts