@replohq/sdk 2.18.0 → 2.18.2

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.
@@ -1,9 +1,9 @@
1
- import type { AnalyticsSink, CartLineEventPayload, CartViewedPayload, CheckoutStartedPayload, ClickPayload, CollectionViewedPayload, LegacyCheckoutStartedPayload, ProductViewedPayload, PurchasePayload } from "../analytics-sink";
1
+ import type { AnalyticsSink, CartLineEventPayload, CartViewedPayload, CheckoutStartedPayload, ClickPayload, CollectionViewedPayload, CustomEventPayload, LegacyCheckoutStartedPayload, ProductViewedPayload, PurchasePayload } from "../analytics-sink";
2
2
  import type { PageMetadata } from "../utils/navigation-tracker";
3
3
  export declare class GA4Sink implements AnalyticsSink {
4
4
  private send;
5
5
  constructor();
6
- customEvent(): Promise<void>;
6
+ customEvent({ event, properties }: CustomEventPayload): Promise<void>;
7
7
  pageView(metadata?: PageMetadata): Promise<void>;
8
8
  purchase({ amount, currency, data }: PurchasePayload): Promise<void>;
9
9
  click({ data }: ClickPayload): Promise<void>;
@@ -13,4 +13,5 @@ export declare class GA4Sink implements AnalyticsSink {
13
13
  viewCart({ data }: CartViewedPayload): Promise<void>;
14
14
  startCheckout(payload: CheckoutStartedPayload | LegacyCheckoutStartedPayload): Promise<void>;
15
15
  viewCollection({ data }: CollectionViewedPayload): Promise<void>;
16
+ private sendCartLine;
16
17
  }
@@ -2,6 +2,22 @@ import { filterUndefined } from "../../_vendor/replo-utils/lib/object.mjs";
2
2
  import { toCheckoutStartedData } from "../legacy-checkout";
3
3
  import { legacyResourceId } from "../utils/analytics-products";
4
4
  import { createEventQueue } from "../utils/analytics-utils";
5
+ function toGa4Item({
6
+ product,
7
+ index
8
+ }) {
9
+ return filterUndefined({
10
+ item_id: legacyResourceId(product.productId),
11
+ item_name: product.productTitle,
12
+ item_variant: product.variantTitle,
13
+ price: product.price,
14
+ quantity: product.quantity,
15
+ index
16
+ });
17
+ }
18
+ function toMoney(amount) {
19
+ return Number(amount.toPrecision(12));
20
+ }
5
21
  class GA4Sink {
6
22
  send;
7
23
  constructor() {
@@ -14,7 +30,8 @@ class GA4Sink {
14
30
  "GA4"
15
31
  );
16
32
  }
17
- async customEvent() {
33
+ async customEvent({ event, properties }) {
34
+ this.send(event, properties);
18
35
  }
19
36
  async pageView(metadata) {
20
37
  const params = metadata ? {
@@ -24,34 +41,60 @@ class GA4Sink {
24
41
  this.send("page_view", params);
25
42
  }
26
43
  async purchase({ amount, currency, data }) {
27
- this.send("purchase", { value: amount, currency, ...data });
44
+ this.send(
45
+ "purchase",
46
+ filterUndefined({
47
+ transaction_id: data.orderId,
48
+ value: toMoney(
49
+ amount - (data.totalTax ?? 0) - (data.totalShipping ?? 0)
50
+ ),
51
+ currency,
52
+ tax: data.totalTax,
53
+ shipping: data.totalShipping,
54
+ coupon: data.discountCodes?.join(","),
55
+ items: data.lineItems?.map((line) => toGa4Item({ product: line }))
56
+ })
57
+ );
28
58
  }
29
59
  async click({ data }) {
30
60
  this.send("select_content", data);
31
61
  }
32
62
  async productViewed({ data }) {
33
- this.send("view_item", {
34
- value: data.price,
35
- currency: data.currency,
36
- ...data
37
- });
63
+ this.send(
64
+ "view_item",
65
+ filterUndefined({
66
+ currency: data.currency,
67
+ value: data.price,
68
+ items: [toGa4Item({ product: data })]
69
+ })
70
+ );
38
71
  }
39
72
  async addToCart({ data }) {
40
- this.send("add_to_cart", {
41
- value: data.price,
42
- currency: data.currency,
43
- ...data
44
- });
73
+ this.sendCartLine({ event: "add_to_cart", line: data });
45
74
  }
46
75
  async removeFromCart({ data }) {
47
- this.send("remove_from_cart", data);
76
+ this.sendCartLine({ event: "remove_from_cart", line: data });
48
77
  }
49
78
  async viewCart({ data }) {
50
- this.send("view_cart", data);
79
+ this.send(
80
+ "view_cart",
81
+ filterUndefined({
82
+ currency: data.currency,
83
+ value: data.subtotal,
84
+ items: data.lineItems.map((line) => toGa4Item({ product: line }))
85
+ })
86
+ );
51
87
  }
52
88
  async startCheckout(payload) {
53
89
  const data = toCheckoutStartedData(payload.data);
54
- this.send("begin_checkout", data);
90
+ this.send(
91
+ "begin_checkout",
92
+ filterUndefined({
93
+ currency: data.currency,
94
+ value: data.subtotal,
95
+ items: data.lineItems.map((line) => toGa4Item({ product: line }))
96
+ })
97
+ );
55
98
  }
56
99
  async viewCollection({ data }) {
57
100
  const { products, ...rest } = data;
@@ -61,18 +104,22 @@ class GA4Sink {
61
104
  currency: data.currency,
62
105
  ...products.length > 0 ? {
63
106
  items: products.map((product, index) => {
64
- return filterUndefined({
65
- item_id: legacyResourceId(product.productId),
66
- item_name: product.productTitle,
67
- item_variant: product.variantTitle,
68
- price: product.price,
69
- index
70
- });
107
+ return toGa4Item({ product, index });
71
108
  })
72
109
  } : {},
73
110
  ...rest
74
111
  });
75
112
  }
113
+ sendCartLine({ event, line }) {
114
+ this.send(
115
+ event,
116
+ filterUndefined({
117
+ currency: line.currency,
118
+ value: line.price === void 0 ? void 0 : toMoney(line.price * line.quantity),
119
+ items: [toGa4Item({ product: line })]
120
+ })
121
+ );
122
+ }
76
123
  }
77
124
  export {
78
125
  GA4Sink
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "2.18.0",
3
+ "version": "2.18.2",
4
4
  "branch": "HEAD",
5
- "commit": "aa9296f4104e4be2271247d775a27fa69288bbe8",
6
- "builtAt": "2026-11-02T03:31:48.722Z"
5
+ "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
6
+ "builtAt": "2026-11-03T21:12:47.031Z"
7
7
  };
@@ -0,0 +1,15 @@
1
+ import { CanopyError } from "../lib/canopy-error";
2
+ /** A loader argument that can't be sent to canopy-api, such as a function. */
3
+ export declare class LoaderArgsError extends CanopyError {
4
+ }
5
+ /**
6
+ * Finds the first function in a loader's args. JSON drops functions silently,
7
+ * so canopy-api would receive the args without that key and reject them. The
8
+ * usual cause is a value exported from a "use client" file and imported into a
9
+ * server component: on the server, every such export is a client reference
10
+ * (a function), never the value itself.
11
+ */
12
+ export declare function getLoaderArgsError({ loaderKey, args, }: {
13
+ loaderKey: string;
14
+ args: Record<string, unknown>;
15
+ }): LoaderArgsError | null;
@@ -0,0 +1,46 @@
1
+ import { CanopyError } from "../lib/canopy-error";
2
+ const CLIENT_REFERENCE_TAG = Symbol.for("react.client.reference");
3
+ class LoaderArgsError extends CanopyError {
4
+ }
5
+ function findFunctionArg(value, path, seen) {
6
+ if (typeof value === "function") {
7
+ return {
8
+ path,
9
+ isClientReference: Reflect.get(value, "$$typeof") === CLIENT_REFERENCE_TAG
10
+ };
11
+ }
12
+ if (typeof value !== "object" || value === null || seen.has(value) || typeof Reflect.get(value, "toJSON") === "function") {
13
+ return null;
14
+ }
15
+ seen.add(value);
16
+ for (const [key, child] of Object.entries(value)) {
17
+ const childPath = Array.isArray(value) ? `${path}[${key}]` : `${path}.${key}`;
18
+ const found = findFunctionArg(child, childPath, seen);
19
+ if (found !== null) {
20
+ return found;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ function getLoaderArgsError({
26
+ loaderKey,
27
+ args
28
+ }) {
29
+ const found = findFunctionArg(args, "args", /* @__PURE__ */ new Set());
30
+ if (found === null) {
31
+ return null;
32
+ }
33
+ const what = found.isClientReference ? "a client reference, not a value" : "a function";
34
+ return new LoaderArgsError({
35
+ message: `[Replo] Loader ${loaderKey}: ${found.path} is ${what}, so it can't be sent to the loader. It was probably imported from a "use client" file into a server component. Move the value into a module without "use client" and import it from both places.`,
36
+ additionalData: {
37
+ loaderKey,
38
+ path: found.path,
39
+ isClientReference: found.isClientReference
40
+ }
41
+ });
42
+ }
43
+ export {
44
+ LoaderArgsError,
45
+ getLoaderArgsError
46
+ };
@@ -21,6 +21,9 @@ export declare class LoaderOutputError extends CanopyError {
21
21
  *
22
22
  * Malformed output is logged with the loader key and the failing paths, then
23
23
  * rejected with a `LoaderOutputError`; callers decide whether to fail soft.
24
+ * Args holding a function are logged and rejected with a `LoaderArgsError`
25
+ * before any request, since JSON would drop them and canopy-api would reject
26
+ * the rest. The log matters because `PrefetchedLoaders` swallows rejections.
24
27
  */
25
28
  export declare function fetchValidatedLoaderOutput({ loaderKey, args, }: {
26
29
  loaderKey: string;
@@ -9,6 +9,7 @@ import {
9
9
  smileRewardsOutputSchema,
10
10
  smileVipTiersOutputSchema
11
11
  } from "./integration-loader-schemas";
12
+ import { getLoaderArgsError } from "./loader-args";
12
13
  import { DATA_LOADER_KEYS } from "./loader-keys";
13
14
  import {
14
15
  contentfulEntriesLoaderOutputSchema,
@@ -93,6 +94,11 @@ async function fetchValidatedLoaderOutput({
93
94
  loaderKey,
94
95
  args
95
96
  }) {
97
+ const argsError = getLoaderArgsError({ loaderKey, args });
98
+ if (argsError !== null) {
99
+ console.error(argsError.message);
100
+ throw argsError;
101
+ }
96
102
  const gateway = await getCanopyApiGateway();
97
103
  const { data } = await gateway.invokeLoader({ loaderKey, args });
98
104
  const knownKey = dataLoaderKeySchema.safeParse(loaderKey);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "2.18.0",
3
+ "version": "2.18.2",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "2.18.0",
6
+ "version": "2.18.2",
7
7
  "branch": "HEAD",
8
- "commit": "aa9296f4104e4be2271247d775a27fa69288bbe8",
9
- "builtAt": "2026-11-02T03:31:48.722Z"
8
+ "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
9
+ "builtAt": "2026-11-03T21:12:47.031Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",