better-ship 0.13.0 → 0.14.0

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.
Files changed (90) hide show
  1. package/dist/{application-DnlgWcOZ.js → application-C4JOf71r.js} +22 -3
  2. package/dist/application-C4JOf71r.js.map +1 -0
  3. package/dist/application.d.ts +3 -3
  4. package/dist/application.js +2 -2
  5. package/dist/better-auth/apple-client-secret.d.ts +1 -1
  6. package/dist/better-auth/apple-client-secret.js +1 -1
  7. package/dist/better-auth/auth-client.js +1 -1
  8. package/dist/better-auth/rate-limit.d.ts +1 -1
  9. package/dist/better-auth/rate-limit.js +1 -1
  10. package/dist/better-auth.js +1 -1
  11. package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
  12. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  13. package/dist/cloudflare/queue.d.ts +2 -2
  14. package/dist/cloudflare/queue.js +2 -2
  15. package/dist/cloudflare/scheduled.d.ts +1 -1
  16. package/dist/cloudflare/scheduled.js +1 -1
  17. package/dist/cloudflare.d.ts +3 -3
  18. package/dist/cloudflare.js +3 -3
  19. package/dist/{core-BiqdOMwU.js → core-hdaNiCOU.js} +21 -2
  20. package/dist/core-hdaNiCOU.js.map +1 -0
  21. package/dist/core.d.ts +2 -2
  22. package/dist/core.js +2 -2
  23. package/dist/http.d.ts +2 -2
  24. package/dist/http.js +2 -2
  25. package/dist/{index-DG4jkwad.d.ts → index-C28HgVfY.d.ts} +56 -3
  26. package/dist/index-C28HgVfY.d.ts.map +1 -0
  27. package/dist/{index-B5O6mNjZ.d.ts → index-G1QG0P5g.d.ts} +34 -2
  28. package/dist/index-G1QG0P5g.d.ts.map +1 -0
  29. package/dist/{outbox-repository-options-BiilQf4_.d.ts → outbox-repository-options-BZjfDqGf.d.ts} +3 -3
  30. package/dist/{outbox-repository-options-BiilQf4_.d.ts.map → outbox-repository-options-BZjfDqGf.d.ts.map} +1 -1
  31. package/dist/{outbox-repository-options-BI75j7b7.js → outbox-repository-options-DOTpJH-e.js} +2 -2
  32. package/dist/{outbox-repository-options-BI75j7b7.js.map → outbox-repository-options-DOTpJH-e.js.map} +1 -1
  33. package/dist/postgres.d.ts +3 -3
  34. package/dist/postgres.js +3 -3
  35. package/dist/posthog/client.d.ts +22 -0
  36. package/dist/posthog/client.d.ts.map +1 -0
  37. package/dist/posthog/client.js +34 -0
  38. package/dist/posthog/client.js.map +1 -0
  39. package/dist/posthog/metrics.d.ts +18 -0
  40. package/dist/posthog/metrics.d.ts.map +1 -0
  41. package/dist/posthog/metrics.js +30 -0
  42. package/dist/posthog/metrics.js.map +1 -0
  43. package/dist/posthog/proxy.d.ts +17 -0
  44. package/dist/posthog/proxy.d.ts.map +1 -0
  45. package/dist/posthog/proxy.js +34 -0
  46. package/dist/posthog/proxy.js.map +1 -0
  47. package/dist/posthog/server.d.ts +37 -0
  48. package/dist/posthog/server.d.ts.map +1 -0
  49. package/dist/posthog/server.js +81 -0
  50. package/dist/posthog/server.js.map +1 -0
  51. package/dist/resend.d.ts +3 -3
  52. package/dist/resend.js +2 -2
  53. package/dist/sentry/client.d.ts +22 -0
  54. package/dist/sentry/client.d.ts.map +1 -0
  55. package/dist/sentry/client.js +48 -0
  56. package/dist/sentry/client.js.map +1 -0
  57. package/dist/sentry/server.d.ts +24 -0
  58. package/dist/sentry/server.d.ts.map +1 -0
  59. package/dist/sentry/server.js +48 -0
  60. package/dist/sentry/server.js.map +1 -0
  61. package/dist/sentry/vite.d.ts +11 -0
  62. package/dist/sentry/vite.d.ts.map +1 -0
  63. package/dist/sentry/vite.js +17 -0
  64. package/dist/sentry/vite.js.map +1 -0
  65. package/dist/tanstack.d.ts +1 -1
  66. package/dist/tanstack.js +2 -2
  67. package/dist/testing/outbox-repository.d.ts +2 -2
  68. package/dist/{webhook-verification-CyBoERFz.d.ts → webhook-verification-D7DIhH6C.d.ts} +2 -2
  69. package/dist/{webhook-verification-CyBoERFz.d.ts.map → webhook-verification-D7DIhH6C.d.ts.map} +1 -1
  70. package/dist/{webhook-verification-CeXUJXVP.js → webhook-verification-trEgyz7I.js} +2 -2
  71. package/dist/{webhook-verification-CeXUJXVP.js.map → webhook-verification-trEgyz7I.js.map} +1 -1
  72. package/package.json +9 -2
  73. package/src/application/analytics-service.ts +21 -0
  74. package/src/application/index.ts +13 -0
  75. package/src/application/metrics-service.ts +28 -0
  76. package/src/application/observability-service.ts +16 -0
  77. package/src/core/analytics-event.ts +37 -0
  78. package/src/core/index.ts +9 -0
  79. package/src/core/missing-configuration.error.ts +12 -0
  80. package/src/infrastructure/posthog/client.ts +41 -0
  81. package/src/infrastructure/posthog/metrics.ts +30 -0
  82. package/src/infrastructure/posthog/proxy.ts +47 -0
  83. package/src/infrastructure/posthog/server.ts +107 -0
  84. package/src/infrastructure/sentry/client.ts +55 -0
  85. package/src/infrastructure/sentry/server.ts +51 -0
  86. package/src/infrastructure/sentry/vite.ts +17 -0
  87. package/dist/application-DnlgWcOZ.js.map +0 -1
  88. package/dist/core-BiqdOMwU.js.map +0 -1
  89. package/dist/index-B5O6mNjZ.d.ts.map +0 -1
  90. package/dist/index-DG4jkwad.d.ts.map +0 -1
@@ -0,0 +1,22 @@
1
+ import { f as LoggerErrorHook } from "../index-G1QG0P5g.js";
2
+ import * as Sentry from "@sentry/tanstackstart-react";
3
+ //#region src/infrastructure/sentry/client.d.ts
4
+ /** Application settings use Sentry's names; enablement and environment belong to the application. */
5
+ export type SentryClientOptions = Sentry.BrowserOptions & {
6
+ readonly dsn: string;
7
+ readonly environment: string;
8
+ readonly enabled: boolean;
9
+ };
10
+ /** Privacy defaults from Porte and browser noise filtering from Typist; explicit SDK options take precedence. */
11
+ export declare function createSentryClientOptions(options: SentryClientOptions): Sentry.BrowserOptions;
12
+ /** Forward logged browser errors once, excluding TanStack navigation control flow. */
13
+ export declare function createSentryLoggerErrorHook(): LoggerErrorHook;
14
+ /** Initialize once in the browser entrypoint before hydration; server imports have no side effects. */
15
+ export declare function initializeSentry(options: SentryClientOptions): ReturnType<typeof Sentry.init>;
16
+ /** Attach tracing once after creating the browser router, as Porte does in getRouter. */
17
+ export declare function instrumentSentryRouter(router: Parameters<typeof Sentry.tanstackRouterBrowserTracingIntegration>[0]): void;
18
+ /** The SDK error boundary; the application supplies its fallback component. */
19
+ export declare const ErrorBoundary: typeof Sentry.ErrorBoundary;
20
+ //#endregion
21
+ export { Sentry };
22
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../../src/infrastructure/sentry/client.ts"],"mappings":";;;;YAMY,sBAAsB,OAAO;WAC9B;WACA;WACA;;;wBAIK,0BAA0B,SAAS,sBAAsB,OAAO;;wBAehE,+BAA+B;;wBAS/B,iBAAiB,SAAS,sBAAsB,kBAAkB,OAAO;;wBAOzE,uBACd,QAAQ,kBAAkB,OAAO;;qBAOtB,sBAAsB,OAAO"}
@@ -0,0 +1,48 @@
1
+ import { s as setLoggerErrorHook } from "../core-hdaNiCOU.js";
2
+ import * as Sentry from "@sentry/tanstackstart-react";
3
+ import { isNotFound, isRedirect } from "@tanstack/react-router";
4
+
5
+ //#region src/infrastructure/sentry/client.ts
6
+ /** Privacy defaults from Porte and browser noise filtering from Typist; explicit SDK options take precedence. */
7
+ function createSentryClientOptions(options) {
8
+ return {
9
+ enableLogs: true,
10
+ ignoreErrors: [/Failed to execute '(insertBefore|removeChild)' on 'Node'/],
11
+ ...options,
12
+ dataCollection: {
13
+ userInfo: false,
14
+ httpBodies: [],
15
+ ...options.dataCollection,
16
+ genAI: {
17
+ inputs: false,
18
+ outputs: false,
19
+ ...options.dataCollection?.genAI
20
+ }
21
+ }
22
+ };
23
+ }
24
+ /** Forward logged browser errors once, excluding TanStack navigation control flow. */
25
+ function createSentryLoggerErrorHook() {
26
+ return ({ error, context }) => {
27
+ if (isNotFound(error) || isRedirect(error)) return;
28
+ const captured = error instanceof Error ? error : /* @__PURE__ */ new Error("Logger received a non-Error value");
29
+ Sentry.captureException(captured, { extra: context });
30
+ };
31
+ }
32
+ /** Initialize once in the browser entrypoint before hydration; server imports have no side effects. */
33
+ function initializeSentry(options) {
34
+ const client = Sentry.init(createSentryClientOptions(options));
35
+ setLoggerErrorHook(createSentryLoggerErrorHook());
36
+ return client;
37
+ }
38
+ /** Attach tracing once after creating the browser router, as Porte does in getRouter. */
39
+ function instrumentSentryRouter(router) {
40
+ if (router.isServer) return;
41
+ Sentry.addIntegration(Sentry.tanstackRouterBrowserTracingIntegration(router));
42
+ }
43
+ /** The SDK error boundary; the application supplies its fallback component. */
44
+ const ErrorBoundary = Sentry.ErrorBoundary;
45
+
46
+ //#endregion
47
+ export { ErrorBoundary, Sentry, createSentryClientOptions, createSentryLoggerErrorHook, initializeSentry, instrumentSentryRouter };
48
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/infrastructure/sentry/client.ts"],"sourcesContent":["import * as Sentry from '@sentry/tanstackstart-react'\nimport { isNotFound, isRedirect } from '@tanstack/react-router'\n\nimport { setLoggerErrorHook, type LoggerErrorHook } from '@/core'\n\n/** Application settings use Sentry's names; enablement and environment belong to the application. */\nexport type SentryClientOptions = Sentry.BrowserOptions & {\n readonly dsn: string\n readonly environment: string\n readonly enabled: boolean\n}\n\n/** Privacy defaults from Porte and browser noise filtering from Typist; explicit SDK options take precedence. */\nexport function createSentryClientOptions(options: SentryClientOptions): Sentry.BrowserOptions {\n return {\n enableLogs: true,\n ignoreErrors: [/Failed to execute '(insertBefore|removeChild)' on 'Node'/],\n ...options,\n dataCollection: {\n userInfo: false,\n httpBodies: [],\n ...options.dataCollection,\n genAI: { inputs: false, outputs: false, ...options.dataCollection?.genAI },\n },\n }\n}\n\n/** Forward logged browser errors once, excluding TanStack navigation control flow. */\nexport function createSentryLoggerErrorHook(): LoggerErrorHook {\n return ({ error, context }) => {\n if (isNotFound(error) || isRedirect(error)) return\n const captured = error instanceof Error ? error : new Error('Logger received a non-Error value')\n Sentry.captureException(captured, { extra: context })\n }\n}\n\n/** Initialize once in the browser entrypoint before hydration; server imports have no side effects. */\nexport function initializeSentry(options: SentryClientOptions): ReturnType<typeof Sentry.init> {\n const client = Sentry.init(createSentryClientOptions(options))\n setLoggerErrorHook(createSentryLoggerErrorHook())\n return client\n}\n\n/** Attach tracing once after creating the browser router, as Porte does in getRouter. */\nexport function instrumentSentryRouter(\n router: Parameters<typeof Sentry.tanstackRouterBrowserTracingIntegration>[0],\n): void {\n if (router.isServer) return\n Sentry.addIntegration(Sentry.tanstackRouterBrowserTracingIntegration(router))\n}\n\n/** The SDK error boundary; the application supplies its fallback component. */\nexport const ErrorBoundary: typeof Sentry.ErrorBoundary = Sentry.ErrorBoundary\n\nexport { Sentry }\n"],"mappings":";;;;;;AAaA,SAAgB,0BAA0B,SAAqD;CAC7F,OAAO;EACL,YAAY;EACZ,cAAc,CAAC,0DAA0D;EACzE,GAAG;EACH,gBAAgB;GACd,UAAU;GACV,YAAY,CAAC;GACb,GAAG,QAAQ;GACX,OAAO;IAAE,QAAQ;IAAO,SAAS;IAAO,GAAG,QAAQ,gBAAgB;GAAM;EAC3E;CACF;AACF;;AAGA,SAAgB,8BAA+C;CAC7D,QAAQ,EAAE,OAAO,cAAc;EAC7B,IAAI,WAAW,KAAK,KAAK,WAAW,KAAK,GAAG;EAC5C,MAAM,WAAW,iBAAiB,QAAQ,wBAAQ,IAAI,MAAM,mCAAmC;EAC/F,OAAO,iBAAiB,UAAU,EAAE,OAAO,QAAQ,CAAC;CACtD;AACF;;AAGA,SAAgB,iBAAiB,SAA8D;CAC7F,MAAM,SAAS,OAAO,KAAK,0BAA0B,OAAO,CAAC;CAC7D,mBAAmB,4BAA4B,CAAC;CAChD,OAAO;AACT;;AAGA,SAAgB,uBACd,QACM;CACN,IAAI,OAAO,UAAU;CACrB,OAAO,eAAe,OAAO,wCAAwC,MAAM,CAAC;AAC9E;;AAGA,MAAa,gBAA6C,OAAO"}
@@ -0,0 +1,24 @@
1
+ import { f as LoggerErrorHook } from "../index-G1QG0P5g.js";
2
+ import { H as ObservabilityScope, U as ObservabilityService, W as ObservabilityUser } from "../index-C28HgVfY.js";
3
+ import * as Sentry from "@sentry/cloudflare";
4
+ //#region src/infrastructure/sentry/server.d.ts
5
+ /** Application-owned deployment settings with the full Cloudflare SDK option contract. */
6
+ export type SentryServerOptions = Sentry.CloudflareOptions & {
7
+ readonly dsn: string;
8
+ readonly environment: string;
9
+ readonly enabled: boolean;
10
+ };
11
+ /** Options for Workers, Durable Objects, and Workflows, ported from Typist and Porte. */
12
+ export declare function createSentryOptions(options: SentryServerOptions): Sentry.CloudflareOptions;
13
+ /** Register with setLoggerErrorHook once at startup; an explicit user applies only to this report. */
14
+ export declare function createSentryLoggerErrorHook(): LoggerErrorHook;
15
+ /** Identity and scope adapter from Typist and Porte; call inside Sentry's instrumented runtime. */
16
+ export declare class SentryObservabilityService implements ObservabilityService {
17
+ /** Attribute subsequent reports in the current isolation scope. */
18
+ setUser(user: ObservabilityUser): void;
19
+ /** Await work in a separate scope for each queue message or concurrent operation. */
20
+ withIsolationScope<T>(callback: (scope: ObservabilityScope) => T | Promise<T>): Promise<T>;
21
+ }
22
+ //#endregion
23
+ export { Sentry };
24
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/infrastructure/sentry/server.ts"],"mappings":";;;;;YAMY,sBAAsB,OAAO;WAC9B;WACA;WACA;;;wBAIK,oBAAoB,SAAS,sBAAsB,OAAO;;wBAc1D,+BAA+B;;qBAWlC,sCAAsC;;EAEjD,QAAQ,MAAM;;EAKR,mBAAmB,GAAG,WAAW,OAAO,uBAAuB,IAAI,QAAQ,KAAK,QAAQ"}
@@ -0,0 +1,48 @@
1
+ import * as Sentry from "@sentry/cloudflare";
2
+
3
+ //#region src/infrastructure/sentry/server.ts
4
+ /** Options for Workers, Durable Objects, and Workflows, ported from Typist and Porte. */
5
+ function createSentryOptions(options) {
6
+ return {
7
+ enableLogs: true,
8
+ ...options,
9
+ dataCollection: {
10
+ userInfo: false,
11
+ httpBodies: [],
12
+ ...options.dataCollection,
13
+ genAI: {
14
+ inputs: false,
15
+ outputs: false,
16
+ ...options.dataCollection?.genAI
17
+ }
18
+ }
19
+ };
20
+ }
21
+ /** Register with setLoggerErrorHook once at startup; an explicit user applies only to this report. */
22
+ function createSentryLoggerErrorHook() {
23
+ return ({ error, distinctId, context }) => {
24
+ if (distinctId !== void 0) {
25
+ Sentry.captureException(error, {
26
+ extra: context,
27
+ user: { id: distinctId }
28
+ });
29
+ return;
30
+ }
31
+ Sentry.captureException(error, { extra: context });
32
+ };
33
+ }
34
+ /** Identity and scope adapter from Typist and Porte; call inside Sentry's instrumented runtime. */
35
+ var SentryObservabilityService = class {
36
+ /** Attribute subsequent reports in the current isolation scope. */
37
+ setUser(user) {
38
+ Sentry.setUser(user);
39
+ }
40
+ /** Await work in a separate scope for each queue message or concurrent operation. */
41
+ async withIsolationScope(callback) {
42
+ return await Sentry.withIsolationScope(callback);
43
+ }
44
+ };
45
+
46
+ //#endregion
47
+ export { Sentry, SentryObservabilityService, createSentryLoggerErrorHook, createSentryOptions };
48
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../../src/infrastructure/sentry/server.ts"],"sourcesContent":["import * as Sentry from '@sentry/cloudflare'\n\nimport type { ObservabilityScope, ObservabilityService, ObservabilityUser } from '@/application'\nimport type { LoggerErrorHook } from '@/core'\n\n/** Application-owned deployment settings with the full Cloudflare SDK option contract. */\nexport type SentryServerOptions = Sentry.CloudflareOptions & {\n readonly dsn: string\n readonly environment: string\n readonly enabled: boolean\n}\n\n/** Options for Workers, Durable Objects, and Workflows, ported from Typist and Porte. */\nexport function createSentryOptions(options: SentryServerOptions): Sentry.CloudflareOptions {\n return {\n enableLogs: true,\n ...options,\n dataCollection: {\n userInfo: false,\n httpBodies: [],\n ...options.dataCollection,\n genAI: { inputs: false, outputs: false, ...options.dataCollection?.genAI },\n },\n }\n}\n\n/** Register with setLoggerErrorHook once at startup; an explicit user applies only to this report. */\nexport function createSentryLoggerErrorHook(): LoggerErrorHook {\n return ({ error, distinctId, context }) => {\n if (distinctId !== undefined) {\n Sentry.captureException(error, { extra: context, user: { id: distinctId } })\n return\n }\n Sentry.captureException(error, { extra: context })\n }\n}\n\n/** Identity and scope adapter from Typist and Porte; call inside Sentry's instrumented runtime. */\nexport class SentryObservabilityService implements ObservabilityService {\n /** Attribute subsequent reports in the current isolation scope. */\n setUser(user: ObservabilityUser): void {\n Sentry.setUser(user)\n }\n\n /** Await work in a separate scope for each queue message or concurrent operation. */\n async withIsolationScope<T>(callback: (scope: ObservabilityScope) => T | Promise<T>): Promise<T> {\n return await Sentry.withIsolationScope(callback)\n }\n}\n\nexport { Sentry }\n"],"mappings":";;;;AAaA,SAAgB,oBAAoB,SAAwD;CAC1F,OAAO;EACL,YAAY;EACZ,GAAG;EACH,gBAAgB;GACd,UAAU;GACV,YAAY,CAAC;GACb,GAAG,QAAQ;GACX,OAAO;IAAE,QAAQ;IAAO,SAAS;IAAO,GAAG,QAAQ,gBAAgB;GAAM;EAC3E;CACF;AACF;;AAGA,SAAgB,8BAA+C;CAC7D,QAAQ,EAAE,OAAO,YAAY,cAAc;EACzC,IAAI,eAAe,QAAW;GAC5B,OAAO,iBAAiB,OAAO;IAAE,OAAO;IAAS,MAAM,EAAE,IAAI,WAAW;GAAE,CAAC;GAC3E;EACF;EACA,OAAO,iBAAiB,OAAO,EAAE,OAAO,QAAQ,CAAC;CACnD;AACF;;AAGA,IAAa,6BAAb,MAAwE;;CAEtE,QAAQ,MAA+B;EACrC,OAAO,QAAQ,IAAI;CACrB;;CAGA,MAAM,mBAAsB,UAAqE;EAC/F,OAAO,MAAM,OAAO,mBAAmB,QAAQ;CACjD;AACF"}
@@ -0,0 +1,11 @@
1
+ import { SentryTanstackStartOptions, sentryTanstackStart as sentryTanstackStart$1 } from "@sentry/tanstackstart-react/vite";
2
+ //#region src/infrastructure/sentry/vite.d.ts
3
+ /** Sentry's Vite options; the SDK creates the tunnel route and sets the browser tunnel URL. */
4
+ export type SentryViteOptions = SentryTanstackStartOptions;
5
+ /**
6
+ * Add after tanstackStart() in Vite. The SDK enables a same-origin tunnel by default here.
7
+ * Set tunnelRoute.allowedDsns when the browser and Worker use different Sentry projects.
8
+ */
9
+ export declare function sentryTanstackStart(options?: SentryViteOptions): ReturnType<typeof sentryTanstackStart$1>;
10
+ //#endregion
11
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","names":[],"sources":["../../src/infrastructure/sentry/vite.ts"],"mappings":";;;YAMY,oBAAoB;;;;;wBAMhB,oBACd,UAAS,oBACR,kBAAkB"}
@@ -0,0 +1,17 @@
1
+ import { sentryTanstackStart as sentryTanstackStart$1 } from "@sentry/tanstackstart-react/vite";
2
+
3
+ //#region src/infrastructure/sentry/vite.ts
4
+ /**
5
+ * Add after tanstackStart() in Vite. The SDK enables a same-origin tunnel by default here.
6
+ * Set tunnelRoute.allowedDsns when the browser and Worker use different Sentry projects.
7
+ */
8
+ function sentryTanstackStart(options = {}) {
9
+ return sentryTanstackStart$1({
10
+ ...options,
11
+ tunnelRoute: options.tunnelRoute ?? true
12
+ });
13
+ }
14
+
15
+ //#endregion
16
+ export { sentryTanstackStart };
17
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","names":["sentryPlugin"],"sources":["../../src/infrastructure/sentry/vite.ts"],"sourcesContent":["import {\n sentryTanstackStart as sentryPlugin,\n type SentryTanstackStartOptions,\n} from '@sentry/tanstackstart-react/vite'\n\n/** Sentry's Vite options; the SDK creates the tunnel route and sets the browser tunnel URL. */\nexport type SentryViteOptions = SentryTanstackStartOptions\n\n/**\n * Add after tanstackStart() in Vite. The SDK enables a same-origin tunnel by default here.\n * Set tunnelRoute.allowedDsns when the browser and Worker use different Sentry projects.\n */\nexport function sentryTanstackStart(\n options: SentryViteOptions = {},\n): ReturnType<typeof sentryPlugin> {\n return sentryPlugin({ ...options, tunnelRoute: options.tunnelRoute ?? true })\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,oBACd,UAA6B,CAAC,GACG;CACjC,OAAOA,sBAAa;EAAE,GAAG;EAAS,aAAa,QAAQ,eAAe;CAAK,CAAC;AAC9E"}
@@ -1,4 +1,4 @@
1
- import { st as ErrorTable } from "./index-B5O6mNjZ.js";
1
+ import { st as ErrorTable } from "./index-G1QG0P5g.js";
2
2
  import { Register } from "@tanstack/react-start";
3
3
  import { BetterAuthCookies } from "better-auth";
4
4
  //#region src/infrastructure/tanstack/request-context.d.ts
package/dist/tanstack.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as AppError, L as ValidationError, U as AuthenticationError, V as StaleSessionError, a as createLogger } from "./core-BiqdOMwU.js";
1
+ import { L as ValidationError, U as AuthenticationError, V as StaleSessionError, a as createLogger, tt as AppError } from "./core-hdaNiCOU.js";
2
2
  import { z } from "zod";
3
3
  import { getReasonPhrase } from "http-status-codes";
4
+ import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
4
5
  import { createMiddleware } from "@tanstack/react-start";
5
6
  import { getSessionCookie, parseCookies } from "better-auth/cookies";
6
- import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
7
7
 
8
8
  //#region src/infrastructure/tanstack/auth.middleware.ts
9
9
  const logger$1 = createLogger("auth-middleware");
@@ -1,5 +1,5 @@
1
- import { G as DomainEvent } from "../index-B5O6mNjZ.js";
2
- import { A as IOutboxRepository } from "../index-DG4jkwad.js";
1
+ import { G as DomainEvent } from "../index-G1QG0P5g.js";
2
+ import { A as IOutboxRepository } from "../index-C28HgVfY.js";
3
3
  //#region src/infrastructure/testing/outbox-repository-contract.d.ts
4
4
  /** Stored fields observed by the adapter contract, independent of a database library. */
5
5
  export type OutboxContractRow = {
@@ -1,4 +1,4 @@
1
- import { Wt as AppError } from "./index-B5O6mNjZ.js";
1
+ import { Zt as AppError } from "./index-G1QG0P5g.js";
2
2
  //#region src/infrastructure/http/webhook-verification.d.ts
3
3
  /** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
4
4
  declare class WebhookVerificationError extends AppError {
@@ -26,4 +26,4 @@ interface WebhookVerifier<Event> {
26
26
  }
27
27
  //#endregion
28
28
  export { WebhookVerificationError as n, WebhookVerifier as r, WebhookVerification as t };
29
- //# sourceMappingURL=webhook-verification-CyBoERFz.d.ts.map
29
+ //# sourceMappingURL=webhook-verification-D7DIhH6C.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-verification-CyBoERFz.d.ts","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"mappings":";;;cAGa,iCAAiC;WAMjC;WALF;WACA;;EAGT,YACW,qEACT;;;KAOQ,oBAAoB;WACjB;WAA2B,OAAO;;WAClC;WAA6B;;WAC7B;WAA8B,OAAO;;;UAGnC,gBAAgB;;EAE/B,OAAO,cAAc,SAAS,UAAU,QAAQ,oBAAoB"}
1
+ {"version":3,"file":"webhook-verification-D7DIhH6C.d.ts","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"mappings":";;;cAGa,iCAAiC;WAMjC;WALF;WACA;;EAGT,YACW,qEACT;;;KAOQ,oBAAoB;WACjB;WAA2B,OAAO;;WAClC;WAA6B;;WAC7B;WAA8B,OAAO;;;UAGnC,gBAAgB;;EAE/B,OAAO,cAAc,SAAS,UAAU,QAAQ,oBAAoB"}
@@ -1,4 +1,4 @@
1
- import { $ as AppError } from "./core-BiqdOMwU.js";
1
+ import { tt as AppError } from "./core-hdaNiCOU.js";
2
2
 
3
3
  //#region src/infrastructure/http/webhook-verification.ts
4
4
  /** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
@@ -15,4 +15,4 @@ var WebhookVerificationError = class extends AppError {
15
15
 
16
16
  //#endregion
17
17
  export { WebhookVerificationError as t };
18
- //# sourceMappingURL=webhook-verification-CeXUJXVP.js.map
18
+ //# sourceMappingURL=webhook-verification-trEgyz7I.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-verification-CeXUJXVP.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** A verified event, an ignored event, or an invalid request. */\nexport type WebhookVerification<Event> =\n | { readonly outcome: 'event'; readonly event: Event }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'rejected'; readonly error: WebhookVerificationError }\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Event> {\n /** Verify the exact body before parsing it. Return invalid requests; throw operational failures. */\n verify(body: string, headers: Headers): Promise<WebhookVerification<Event>>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
1
+ {"version":3,"file":"webhook-verification-trEgyz7I.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** A verified event, an ignored event, or an invalid request. */\nexport type WebhookVerification<Event> =\n | { readonly outcome: 'event'; readonly event: Event }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'rejected'; readonly error: WebhookVerificationError }\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Event> {\n /** Verify the exact body before parsing it. Return invalid requests; throw operational failures. */\n verify(body: string, headers: Headers): Promise<WebhookVerification<Event>>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -29,7 +29,14 @@
29
29
  "./core": "./dist/core.js",
30
30
  "./http": "./dist/http.js",
31
31
  "./postgres": "./dist/postgres.js",
32
+ "./posthog/client": "./dist/posthog/client.js",
33
+ "./posthog/metrics": "./dist/posthog/metrics.js",
34
+ "./posthog/proxy": "./dist/posthog/proxy.js",
35
+ "./posthog/server": "./dist/posthog/server.js",
32
36
  "./resend": "./dist/resend.js",
37
+ "./sentry/client": "./dist/sentry/client.js",
38
+ "./sentry/server": "./dist/sentry/server.js",
39
+ "./sentry/vite": "./dist/sentry/vite.js",
33
40
  "./tanstack": "./dist/tanstack.js",
34
41
  "./tanstack/server": "./dist/tanstack/server.js",
35
42
  "./testing/outbox-repository": "./dist/testing/outbox-repository.js",
@@ -58,7 +65,7 @@
58
65
  "@tanstack/start-storage-context": "^1.167.31",
59
66
  "@types/node": "^26.5.1",
60
67
  "@types/react": "^19.3.0",
61
- "@vitest/browser-playwright": "^5.0.0",
68
+ "@vitest/browser-playwright": "^4.1.11",
62
69
  "auth": "^1.7.4",
63
70
  "better-auth": "^1.7.4",
64
71
  "drizzle-kit": "^0.31.10",
@@ -0,0 +1,21 @@
1
+ import type { AnalyticsEventIdentity, AnalyticsEventInput, AnalyticsProperties } from '@/core'
2
+
3
+ /** Product analytics without an SDK dependency, ported from Typist and Porte. */
4
+ export interface AnalyticsService<TEvent extends AnalyticsEventInput = AnalyticsEventInput> {
5
+ /** Attach traits to one person. */
6
+ identify(distinctId: string, properties?: AnalyticsProperties): void
7
+ /** Merge a previous ID into this person; the application must establish that both identify the same person. */
8
+ alias(distinctId: string, previousDistinctId: string): void
9
+ /** Schedule an event for a person; returning does not confirm remote acceptance. */
10
+ track(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void
11
+ /** Schedule an event without creating a person profile for its distinct ID. */
12
+ trackAnonymous(event: TEvent, distinctId: string, identity?: AnalyticsEventIdentity): void
13
+ }
14
+
15
+ /** Reports to nobody. The composition root selects this service when analytics is disabled. */
16
+ export const NULL_ANALYTICS: AnalyticsService = {
17
+ identify: () => {},
18
+ alias: () => {},
19
+ track: () => {},
20
+ trackAnonymous: () => {},
21
+ }
@@ -1,4 +1,17 @@
1
1
  export type { CommandHandler, EventHandler, QueryHandler } from './handlers.ts'
2
+ export { NULL_ANALYTICS, type AnalyticsService } from './analytics-service.ts'
3
+ export {
4
+ NULL_METRICS_SERVICE,
5
+ type MetricsService,
6
+ type MetricOptions,
7
+ type MetricAttributes,
8
+ type MetricAttributeValue,
9
+ } from './metrics-service.ts'
10
+ export type {
11
+ ObservabilityService,
12
+ ObservabilityScope,
13
+ ObservabilityUser,
14
+ } from './observability-service.ts'
2
15
  export {
3
16
  RecordingEmailService,
4
17
  type IEmailService,
@@ -0,0 +1,28 @@
1
+ /** A metric dimension; use a small, bounded value set and never user or request IDs. */
2
+ export type MetricAttributeValue = string | number | boolean
3
+
4
+ /** Dimensions shared by observations in one time series. */
5
+ export type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>
6
+
7
+ /** Unit and dimensions for an operational measurement. */
8
+ export interface MetricOptions {
9
+ readonly attributes?: MetricAttributes
10
+ readonly unit?: string
11
+ }
12
+
13
+ /** Operational measurements, ported from Typist's metrics-service.ts; applications own metric names. */
14
+ export interface MetricsService<TName extends string = string> {
15
+ /** Add a nonnegative value to a total; the default increment is one. */
16
+ count(name: TName, value?: number, options?: MetricOptions): void
17
+ /** Record a current value. */
18
+ gauge(name: TName, value: number, options?: MetricOptions): void
19
+ /** Record one observation in a distribution. */
20
+ histogram(name: TName, value: number, options?: MetricOptions): void
21
+ }
22
+
23
+ /** Records nothing when the composition root disables metrics. */
24
+ export const NULL_METRICS_SERVICE: MetricsService = {
25
+ count: () => {},
26
+ gauge: () => {},
27
+ histogram: () => {},
28
+ }
@@ -0,0 +1,16 @@
1
+ /** The identity an error report belongs to. */
2
+ export interface ObservabilityUser {
3
+ readonly id: string
4
+ }
5
+
6
+ /** Identity within one isolated execution. */
7
+ export interface ObservabilityScope {
8
+ /** Attribute reports in this scope to one person. */
9
+ setUser(user: ObservabilityUser): void
10
+ }
11
+
12
+ /** Error identity and isolation, ported from Porte's sentry-observability-service.ts. */
13
+ export interface ObservabilityService extends ObservabilityScope {
14
+ /** Await work in its own scope so concurrent executions do not share identity. */
15
+ withIsolationScope<T>(callback: (scope: ObservabilityScope) => T | Promise<T>): Promise<T>
16
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod'
2
+
3
+ /** An event name and its application-owned properties, ported from Typist's analytics/types.ts. */
4
+ export interface AnalyticsEventInput<
5
+ TName extends string = string,
6
+ TProperties extends object = object,
7
+ > {
8
+ readonly name: TName
9
+ readonly properties: TProperties
10
+ }
11
+
12
+ /** A value supported by analytics person properties. */
13
+ export type AnalyticsProperty =
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | undefined
19
+ | readonly AnalyticsProperty[]
20
+ | AnalyticsProperties
21
+
22
+ /** Person properties supplied by the application. */
23
+ export interface AnalyticsProperties {
24
+ readonly [name: string]: AnalyticsProperty
25
+ }
26
+
27
+ /** Parse a persisted analytics event UUID; the SDK generates ordinary event UUIDs itself. */
28
+ export const AnalyticsEventIdSchema = z.uuid().brand<'AnalyticsEventId'>()
29
+
30
+ /** The identity of one analytics event across repeated application deliveries. */
31
+ export type AnalyticsEventId = z.infer<typeof AnalyticsEventIdSchema>
32
+
33
+ /** Supply both original values when replaying one event; never share them across different occurrences. */
34
+ export interface AnalyticsEventIdentity {
35
+ readonly uuid: AnalyticsEventId
36
+ readonly timestamp: Date
37
+ }
package/src/core/index.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  export { AppError } from './app-error.ts'
2
+ export { MissingConfigurationError } from './missing-configuration.error.ts'
3
+ export {
4
+ AnalyticsEventIdSchema,
5
+ type AnalyticsEventId,
6
+ type AnalyticsEventIdentity,
7
+ type AnalyticsEventInput,
8
+ type AnalyticsProperties,
9
+ type AnalyticsProperty,
10
+ } from './analytics-event.ts'
2
11
  export { EmailAddressSchema, type EmailAddress } from './email-address.ts'
3
12
  export {
4
13
  emailIdempotencyKey,
@@ -0,0 +1,12 @@
1
+ import { AppError } from './app-error.ts'
2
+
3
+ /** Required configuration is absent. Ported from Porte's configuration.errors.ts. */
4
+ export class MissingConfigurationError extends AppError {
5
+ readonly _tag = 'MissingConfigurationError'
6
+ readonly classification = 'terminal'
7
+
8
+ /** Name the missing setting without including its value. */
9
+ constructor(readonly key: string) {
10
+ super(`${key} is required`)
11
+ }
12
+ }
@@ -0,0 +1,41 @@
1
+ import posthog, { type PostHog, type PostHogConfig } from 'posthog-js'
2
+
3
+ /** The application controls enablement; disabled initialization needs no project key. */
4
+ export type PostHogClientConfig =
5
+ | { readonly enabled: false }
6
+ | {
7
+ readonly enabled: true
8
+ readonly apiKey: string
9
+ readonly environment: string
10
+ readonly source: string
11
+ readonly options?: Partial<PostHogConfig>
12
+ }
13
+
14
+ /** Browser options from Typist and Porte; SDK option names and application overrides stay intact. */
15
+ export function createPostHogClientOptions(
16
+ config: Extract<PostHogClientConfig, { enabled: true }>,
17
+ ): Partial<PostHogConfig> {
18
+ return {
19
+ api_host: '/relay',
20
+ ui_host: 'https://us.posthog.com',
21
+ person_profiles: 'identified_only',
22
+ defaults: '2026-08-29',
23
+ disable_surveys: true,
24
+ advanced_disable_feature_flags: true,
25
+ capture_performance: { web_vitals: false },
26
+ ...config.options,
27
+ loaded: (client) => {
28
+ client.register({ environment: config.environment, source: config.source })
29
+ config.options?.loaded?.(client)
30
+ },
31
+ }
32
+ }
33
+
34
+ /** Initialize once in the browser entrypoint; pass the returned SDK client to PostHogProvider. */
35
+ export function initializePostHog(config: PostHogClientConfig): PostHog {
36
+ if (!config.enabled) return posthog
37
+ posthog.init(config.apiKey, createPostHogClientOptions(config))
38
+ return posthog
39
+ }
40
+
41
+ export { PostHogProvider, usePostHog } from '@posthog/react'
@@ -0,0 +1,30 @@
1
+ import type { PostHog } from 'posthog-node'
2
+
3
+ import type { MetricOptions, MetricsService } from '@/application'
4
+
5
+ /** PostHog Metrics with Worker lifetime management, ported from Typist's metrics-service.ts. */
6
+ export class PostHogMetricsService implements MetricsService {
7
+ /** Use the analytics client's metrics; the runtime owns each flush promise. */
8
+ constructor(
9
+ private readonly client: Pick<PostHog, 'metrics'>,
10
+ private readonly waitUntil: (promise: Promise<unknown>) => void,
11
+ ) {}
12
+
13
+ /** Record a total and schedule its flush. */
14
+ count(name: string, value = 1, options?: MetricOptions): void {
15
+ this.client.metrics.count(name, value, options)
16
+ this.waitUntil(this.client.metrics.flush())
17
+ }
18
+
19
+ /** Record a current value and schedule its flush. */
20
+ gauge(name: string, value: number, options?: MetricOptions): void {
21
+ this.client.metrics.gauge(name, value, options)
22
+ this.waitUntil(this.client.metrics.flush())
23
+ }
24
+
25
+ /** Record an observation and schedule its flush. */
26
+ histogram(name: string, value: number, options?: MetricOptions): void {
27
+ this.client.metrics.histogram(name, value, options)
28
+ this.waitUntil(this.client.metrics.flush())
29
+ }
30
+ }
@@ -0,0 +1,47 @@
1
+ /** Runtime resources and deployment hosts for Typist's same-origin proxy. */
2
+ export interface PostHogProxyOptions {
3
+ readonly cache: Pick<Cache, 'match' | 'put'>
4
+ readonly waitUntil: (promise: Promise<void>) => void
5
+ readonly basePath?: string
6
+ readonly ingestHost?: string
7
+ readonly assetHost?: string
8
+ readonly fetch?: typeof globalThis.fetch
9
+ }
10
+
11
+ /**
12
+ * Mount at /relay/$ with GET, POST, and OPTIONS handlers. Supply Cloudflare's asset cache and waitUntil.
13
+ * The application must use the same base path in the browser's api_host option.
14
+ */
15
+ export async function proxyPostHogRequest(
16
+ request: Request,
17
+ options: PostHogProxyOptions,
18
+ ): Promise<Response> {
19
+ const url = new URL(request.url)
20
+ const basePath = options.basePath ?? '/relay'
21
+ if (!url.pathname.startsWith(`${basePath}/`)) return new Response(null, { status: 404 })
22
+ const pathname = url.pathname.slice(basePath.length)
23
+ const pathWithSearch = pathname + url.search
24
+ const send = options.fetch ?? globalThis.fetch
25
+
26
+ if (pathname.startsWith('/static/') || pathname.startsWith('/array/')) {
27
+ const cached = await options.cache.match(request)
28
+ if (cached) return cached
29
+ const response = await send(
30
+ `https://${options.assetHost ?? 'us-assets.i.posthog.com'}${pathWithSearch}`,
31
+ )
32
+ options.waitUntil(options.cache.put(request, response.clone()))
33
+ return response
34
+ }
35
+
36
+ const headers = new Headers(request.headers)
37
+ headers.delete('cookie')
38
+ headers.delete('authorization')
39
+ headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP') ?? '')
40
+ return await send(`https://${options.ingestHost ?? 'us.i.posthog.com'}${pathWithSearch}`, {
41
+ method: request.method,
42
+ headers,
43
+ body:
44
+ request.method !== 'GET' && request.method !== 'HEAD' ? await request.arrayBuffer() : null,
45
+ redirect: request.redirect,
46
+ })
47
+ }