Skip to content

Fix magento#1229

Open
guitavano wants to merge 449 commits intomainfrom
fix-magento
Open

Fix magento#1229
guitavano wants to merge 449 commits intomainfrom
fix-magento

Conversation

@guitavano
Copy link
Contributor

@guitavano guitavano commented Jun 5, 2025

What is this Contribution About?

Please provide a brief description of the changes or enhancements you are proposing in this pull request.

Issue Link

Please link to the relevant issue that this pull request addresses:

Loom Video

Record a quick screencast describing your changes to help the team understand and review your contribution. This will greatly assist in the review process.

Demonstration Link

Provide a link to a branch or environment where this pull request can be tested and seen in action.

Summary by CodeRabbit

  • New Features

    • Introduced pathname slug extraction for URL routing
    • Enhanced pageview tracking with improved event collection and utm data handling
  • Refactor

    • Optimized GraphQL query construction for more efficient GET requests
    • Improved route segment ranking and pattern matching logic

aka-sacci-ccr and others added 30 commits June 13, 2024 12:15
@github-actions
Copy link
Contributor

github-actions bot commented Jun 5, 2025

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.100.5 update
  • 🎉 for Minor 0.101.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 23, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request refactors GraphQL client utilities to improve request handling through helper functions and new types, extends tracking capabilities with pageview and event emissions, introduces URL pathname extraction functionality, modifies route ranking and pattern construction logic, and integrates the new function into the manifest.

Changes

Cohort / File(s) Summary
GraphQL Client Refactoring
utils/graphql.ts
Introduced new GraphQLQueryProps<V> interface and internal helper functions (getMethodAndProps, getParamsAndBody, minifyString, etc.). Made request body optional in GraphQLAPI<D> type. Refactored request construction to dynamically select HTTP method and compute parameters via helpers rather than hard-coded logic. Updated public signature to accept DecoRequestInit parameter.
Tracking & Analytics
website/components/OneDollarStats.tsx
Added exported trackerOriginal constant containing expanded tracking script with pageview monitoring via history.pushState and popstate events, Deco event subscription for flag/page accumulation, and generic event forwarding to window.stonks.event. Integrated payload truncation and richer PageView emissions with utm/path data.
URL & Routing
website/functions/requestToPathname.ts, website/handlers/router.ts, website/manifest.gen.ts
Created new loader function requestToParam to extract pathname slug by removing configured prefix. Modified route ranking to penalize segments starting with "}?" and rewrote URL pattern construction to use URLPattern directly. Registered new function in manifest with module alias.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • mcandeia
  • JonasJesus42

Poem

🐰 Through graphql streams, tracking moonbeams bright,
Routes now rank with dynamic might,
Pathnames extracted, patterns refined,
Fresh helpers and types beautifully designed!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely a template with no concrete information filled in. All required sections remain as placeholders without actual content or links. Fill in the description template with: a concrete summary of the changes made, a link to the relevant issue, a Loom video demonstrating the changes, and a demonstration link to test the implementation.
Title check ❓ Inconclusive The title 'Fix magento' is vague and does not clearly convey the main changes in the changeset, which involve substantial refactoring of GraphQL utilities, route handling, and tracking functionality. Use a more descriptive title that reflects the primary change, such as 'Refactor GraphQL client and route ranking logic' or 'Improve GraphQL request handling and route matching'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-magento

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
website/components/OneDollarStats.tsx (1)

30-33: Exporting full tracker script risks bundle bloat and config drift.

Consider moving trackerOriginal to a separate asset/module or guarding behind dev-only export; it hardcodes https://collector.onedollarstats.com/events while the component uses DEFAULT_COLLECTOR_ADDRESS (https://d.lilstts.com/events). This can confuse consumers if imported.

utils/graphql.ts (3)

159-162: stringfyVariables returns "undefined" for empty input; rename and make optional.

Return undefined when no variables to keep URLs short, and fix spelling.

Apply:

-const stringfyVariables = <V>({
-  variables,
-}: Pick<GraphQLQueryProps<V>, "variables">) => JSON.stringify(variables);
+const stringifyVariables = <V>({
+  variables,
+}: Pick<GraphQLQueryProps<V>, "variables">) =>
+  variables === undefined ? undefined : JSON.stringify(variables);

And update callers:

-  const stringfiedVariables = stringfyVariables<V>({ variables });
+  const stringfiedVariables = stringifyVariables<V>({ variables });

163-179: Use encodeURIComponent and include operationName consistently.

For length estimation, prefer encodeURIComponent and only append operationName when present to avoid inflating length.

-  const urlLength = `${href}?query=${encodeURI(query)}&variables=${
-    encodeURI(
-      variables,
-    )
-  }&operationname=${operationName}`;
+  const qp = [
+    `query=${encodeURIComponent(query)}`,
+    `variables=${encodeURIComponent(variables ?? "")}`,
+    operationName ? `operationName=${encodeURIComponent(operationName)}` : "",
+  ].filter(Boolean).join("&");
+  const urlLength = `${href}?${qp}`;

84-124: Guard against GET with huge fragments arrays.

If fragments is large, isPostMethodRequired may still choose GET before minification is applied to all parts. Consider estimating with the joined+minified query to avoid overly long URLs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1ae949 and d7ce899.

📒 Files selected for processing (5)
  • utils/graphql.ts (4 hunks)
  • website/components/OneDollarStats.tsx (1 hunks)
  • website/functions/requestToPathname.ts (1 hunks)
  • website/handlers/router.ts (3 hunks)
  • website/manifest.gen.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
website/functions/requestToPathname.ts (1)
website/functions/requestToParam.ts (1)
  • RequestURLParam (3-3)
utils/graphql.ts (1)
utils/fetch.ts (1)
  • DecoRequestInit (34-36)
🔇 Additional comments (4)
website/manifest.gen.ts (1)

15-16: Manifest wiring looks correct.

Import and mapping for website/functions/requestToPathname.ts added alongside requestToParam. No action needed.

Also applies to: 64-65

utils/graphql.ts (2)

16-26: Type change to optional body in GraphQLAPI is fine.

This aligns with GET support; no issues spotted.


137-151: Minifier correctly handles nested selections; no changes required.

The test confirms your minifier produces correct output for nested GraphQL selections. The outer braces { checkout(...) { ...Checkout } } are not matched by the first regex (which explicitly requires [^{}]*—no nested braces), but the subsequent whitespace-stripping replacements handle nesting correctly. The minified output is valid: query GetCart($checkoutId:String!){checkout(checkoutId:$checkoutId){...Checkout}}. No fix or test addition is necessary.

website/handlers/router.ts (1)

88-99: I need to verify the URLPattern API specification to understand how the search property should be formatted.

Let me search for more specific information about how URLPattern handles the search property when constructed with a base URL.

Let me search for more specific information about what the .search property returns when a URLPattern is constructed from a URL string without a query component.

Based on my verification, I can now provide the final response.

No issues found with the URLPattern search component formatting.

The URLPattern search property is a string containing the pattern used to match the search part of a URL, and is the normalized value of the search pattern passed to the constructor or the default value ("*"), which matches any search part. When using search in a URLPattern constructor object, the value does not include the leading question mark—for example, const pattern = new URLPattern({ search: "q=baby" }) logs pattern.search as "q=baby".

The code correctly handles both cases: when patternWithDefaultOrigin.search is an empty string (from a URL with no query component), the fallback to "*" maintains consistent formatting since neither value includes a leading ?.

Comment on lines 53 to 74
return {
query: async <D, V>(
{ query = "", fragments = [], variables, operationName }: {
query: string;
fragments?: string[];
variables?: V;
operationName?: string;
},
init?: RequestInit,
{
query = "",
fragments = [],
variables,
operationName,
}: GraphQLQueryProps<V>,
init?: DecoRequestInit,
): Promise<D> => {
const { data, errors } = await http[key as any]({}, {
...init,
body: {
query: [query, ...fragments].join("\n"),
variables: variables as any,
operationName,
},
const { key, props } = getMethodAndProps<V>({
query,
fragments,
variables,
operationName,
url,
});
const { searchParams, body } = getParamsAndBody({ key, props, init });
const { data, errors } = await http[key as any](searchParams, {
...body,
}).then((res) => res.json());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

POST body may not be JSON‑stringified; init.body can override payload.

  • getParamsAndBody returns { body: props, ...init } for POST; if createHttpClient doesn’t stringify, requests may send [object Object] or fail.
  • Spreading ...init after body: props can replace the computed body.

Ensure JSON string body and keep props as the final body.

Apply:

-      const { searchParams, body } = getParamsAndBody({ key, props, init });
-      const { data, errors } = await http[key as any](searchParams, {
-        ...body,
-      }).then((res) => res.json());
+      const { searchParams, init: httpInit } = getParamsAndBody({ key, props, init });
+      const { data, errors } = await http[key as any](searchParams, httpInit)
+        .then((res) => res.json());

And update helper (see next comment).

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +126 to +135
const getParamsAndBody = ({
key,
props,
init,
}: ReturnType<typeof getMethodAndProps> & { init?: DecoRequestInit }) => {
if (key.startsWith("POST")) {
return { searchParams: {}, body: { body: props, ...init } };
}
return { searchParams: { ...props }, body: { ...init } };
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Harden GET/POST param packing and avoid undefined values.

  • For POST, JSON‑stringify the payload and ensure it wins over init.body.
  • For GET, omit keys with undefined to avoid variables=undefined in the URL.

Apply:

-const getParamsAndBody = ({
+const getParamsAndBody = ({
   key,
   props,
   init,
 }: ReturnType<typeof getMethodAndProps> & { init?: DecoRequestInit }) => {
   if (key.startsWith("POST")) {
-    return { searchParams: {}, body: { body: props, ...init } };
+    const bodyJson = JSON.stringify(props);
+    return { searchParams: {}, init: { ...(init ?? {}), body: bodyJson } };
   }
-  return { searchParams: { ...props }, body: { ...init } };
+  const searchParams: Record<string, string> = {};
+  if (props.query) searchParams.query = props.query;
+  if (props.operationName) searchParams.operationName = props.operationName;
+  if (typeof props.variables === "string" && props.variables.length > 0) {
+    searchParams.variables = props.variables;
+  }
+  return { searchParams, init: { ...(init ?? {}) } };
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In utils/graphql.ts around lines 126 to 135, harden the GET/POST param packing
by: for POST requests ensure the request body is a JSON string of props
(JSON.stringify(props)) and that this serialized payload takes precedence over
any init.body (i.e., set body: { body: JSON.stringify(props), ...init } but
ensure init.body is not used if props provided); for GET requests build
searchParams by copying props but filtering out keys whose values are undefined
so you don't produce query entries like variables=undefined (omit
undefined-valued keys before returning searchParams); keep the same return shape
({ searchParams, body }) and preserve other init fields.

Comment on lines +4 to +10
export interface Props {
/**
* @description Path name to remove from the URL - Stringfied regex. Example: "/product/" transform "/product/my-sku" in "my-sku". "/product/|/store" transform "/product/my-sku/store" in "my-sku"
* @format dynamic-options
*/
pathname?: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Doc nit: typo and clarify examples.

“Stringfied” → “Stringified”. Consider clarifying anchoring (prefix-only vs anywhere) in examples to avoid accidental middle replacements.

🤖 Prompt for AI Agents
In website/functions/requestToPathname.ts around lines 4 to 10, fix the
documentation typo ("Stringfied" → "Stringified") and clarify the examples to
state whether the provided string is treated as a full/anchored pattern or can
match anywhere; update the examples to explicitly show anchoring behavior (e.g.,
prefix-only vs anywhere) and mention that the value is a stringified regex so
callers should include anchors (or that the code will add anchors) to avoid
accidental middle replacements.

Comment on lines +16 to +26
const requestToParam: LoaderFunction<
Props,
RequestURLParam,
FunctionContext
> = (req, ctx) => {
const url = new URL(req.url);
const regex = new RegExp("(" + ctx.state.$live.pathname + ")", "g");
return {
data: url.pathname.toString().replace(regex, ""),
};
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Props ignored and regex can over‑match; anchor and escape.

  • Props.pathname is never used; current code relies on ctx.state.$live.pathname.
  • The regex isn’t anchored and may strip matches anywhere in the path; unescaped input can behave unexpectedly.

Use props?.pathname ?? ctx.state.$live.pathname, anchor to start, and fall back safely.

Apply:

-const requestToParam: LoaderFunction<
-  Props,
-  RequestURLParam,
-  FunctionContext
-> = (req, ctx) => {
-  const url = new URL(req.url);
-  const regex = new RegExp("(" + ctx.state.$live.pathname + ")", "g");
-  return {
-    data: url.pathname.toString().replace(regex, ""),
-  };
-};
+const requestToParam: LoaderFunction<
+  Props,
+  RequestURLParam,
+  FunctionContext
+> = (req, ctx, props) => {
+  const url = new URL(req.url);
+  const pattern = (props?.pathname ?? ctx.state.$live.pathname ?? "").trim();
+  if (!pattern) {
+    // strip leading slash only
+    return { data: url.pathname.replace(/^\/+/, "") };
+  }
+  // Anchor to start. If you intend a true regex, keep as-is; otherwise escape:
+  // const safe = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+  const regex = new RegExp(`^(?:${pattern})`);
+  const slug = url.pathname.replace(regex, "").replace(/^\/+/, "");
+  return { data: slug };
+};
🤖 Prompt for AI Agents
In website/functions/requestToPathname.ts around lines 16 to 26, the function
ignores the passed-in props and builds an unescaped, unanchored regex from
ctx.state.$live.pathname which can over-match; instead choose the basePath as
props?.pathname ?? ctx.state.$live.pathname, escape any regex metacharacters in
that value, anchor it to the start of the pathname (prepend ^) so only a leading
match is removed, and use a single replace (not a global replace) to strip the
basePath; ensure you coerce the chosen pathname to a string and handle
null/undefined safely.

Comment on lines +29 to +34
if (routePart.startsWith("}?")) {
if (routePart.includes("*")) {
return acc - 2;
}
return acc - 1;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Validate rank logic for optional/wildcard segments starting with }?.

Ensure your route syntax can actually produce segments that begin with "}?". If not, this branch never triggers or could mask other cases. Add a unit test with representative patterns to confirm ordering.


🏁 Script executed:

#!/bin/bash
# List route templates that contain '}' at segment start
rg -nP '(^|/)}\?' --type=ts -C2 website | sed -n '1,200p'

Length of output: 0


I need to better understand the route template syntax and how segments are constructed. Let me examine the router.ts file and search for test cases or route examples.


🏁 Script executed:

#!/bin/bash
# Read the router.ts file to understand context
cat website/handlers/router.ts

Length of output: 6575


Let me search for test files and route definition examples to understand what route patterns actually exist:


🏁 Script executed:

#!/bin/bash
# Search for test files that might contain route examples
fd -e test.ts -e test.tsx -e _test.ts -o -name '*test*.ts' website | head -20

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Look for route definitions and examples
rg -n "pathTemplate" --type=ts website | head -30

Length of output: 2415


🏁 Script executed:

#!/bin/bash
# Search for what patterns actually contain } character
rg -nP '\}' --type=ts website/handlers/router.ts -C3

Length of output: 6499


🏁 Script executed:

#!/bin/bash
# Look at flags/audience.ts to understand route structure
cat website/flags/audience.ts 2>/dev/null || echo "File not found"

Length of output: 1879


Let me search more broadly for any route patterns that might explain the }? syntax, and check URLPattern usage:


🏁 Script executed:

#!/bin/bash
# Search for actual values assigned to pathTemplate
rg -nP 'pathTemplate\s*[=:]' --type=ts -A1 website | head -50

Length of output: 2659


🏁 Script executed:

#!/bin/bash
# Look for any patterns with { or } characters that might be encoded or escaped
rg -nP '[{}<>]' --type=ts website/loaders/options/routes.ts -C2

Length of output: 814


🏁 Script executed:

#!/bin/bash
# Search for imports or usage of URLPattern to understand the syntax
rg -n "URLPattern\|urlPatternCache" --type=ts website -C2 | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any comments about route syntax or rankRoute logic
rg -n "rankRoute\|route.*syntax\|pattern.*format" --type=ts website -C3

Length of output: 0


Let me search for any test files related to routing to confirm there's no test coverage:


🏁 Script executed:

#!/bin/bash
# Search for any test or spec files related to routing
find website -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*_test.ts" \) | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for rankRoute function usage and calls
rg -n "rankRoute" --type=ts website

Length of output: 199


🏁 Script executed:

#!/bin/bash
# Check if URLPattern is used and what patterns are created
grep -n "URLPattern\|new URLPattern" website/handlers/router.ts

Length of output: 200


Remove dead code from rankRoute or add tests demonstrating when }? pattern can occur.

The }? check in the rankRoute function (line 29) appears unreachable: no route patterns in the codebase use this syntax, and it doesn't match standard URLPattern Web API syntax (which uses :param, *, **). Either remove this branch or add unit tests with concrete route examples that would trigger it.

🤖 Prompt for AI Agents
In website/handlers/router.ts around lines 29 to 34, the branch checking for
routePart.startsWith("}?") appears dead/unreachable; either remove this branch
to simplify rankRoute or add unit tests demonstrating a real route pattern using
"}?". Fix by choosing one: (A) delete the entire if (routePart.startsWith("}?"))
{ ... } block and adjust any dependent logic/tests accordingly, or (B) keep it
but add unit tests with concrete route strings that include the "}?"/wildcard
combinations (including cases with and without "*") that assert the decrement
behavior (acc -2 vs acc -1) so the branch is covered; ensure tests reference
existing route parsing expectations and update comments to explain why the
pattern is supported.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
website/components/OneDollarStats.tsx (2)

51-66: Fix: accidental invocation of subscribe return; add DECO guard; null‑safe page.id

Calling the return value of subscribe (})();) will throw at runtime. Also, window.DECO may not exist yet, and page.id is accessed without a null check.

Apply:

-  globalThis.window.DECO.events.subscribe((event) => {
+  if (globalThis.window.DECO?.events?.subscribe) {
+    globalThis.window.DECO.events.subscribe((event) => {
       if (!event || event.name !== "deco") {
         return;
       }
-      if (event.params) {
-        const { flags, page } = event.params;
+      if (event.params) {
+        const { flags, page } = event.params;
         if (Array.isArray(flags)) {
           for (const flag of flags) {
             props[flag.name] = truncate(flag.value.toString());
           }
         }
-        props["pageId"] = truncate(`${page.id}`);
+        if (page?.id != null) {
+          props["pageId"] = truncate(String(page.id));
+        }
       }
       trackPageview();
-  })();
+    });
+  }

34-47: Prevent duplicate PageView emissions from pushState/popstate + "deco"

Confirmed: website/components/OneDollarStats.tsx both monkey‑patches history.pushState / listens to popstate and also uses the DECO "deco" subscription (subscribe(...)()) to call trackPageview — this can double‑count pageviews on SPA navigations.

Minimal dedupe (skip repeated same href within a short window):

-  const trackPageview = () => globalThis.window.stonks?.view?.(props);
+  let lastHref = "";
+  let lastTs = 0;
+  const trackPageview = () => {
+    const href = location.href.replace(/#.*$/, "");
+    const now = Date.now();
+    if (href === lastHref && now - lastTs < 500) return;
+    lastHref = href; lastTs = now;
+    globalThis.window.stonks?.view?.(props);
+  };

Or remove the pushState/popstate hooks and rely only on the DECO event if that event is guaranteed for all client-side navigations.

🧹 Nitpick comments (6)
website/components/OneDollarStats.tsx (6)

48-50: Truncation says “2000 bytes” but slices 990 characters

Mismatch and not byte‑safe for multi‑byte chars.

Byte‑aware truncation:

-  // 2000 bytes limit
-  const truncate = (str: string) => `${str}`.slice(0, 990);
+  // 2000 bytes limit (UTF‑8)
+  const truncate = (val: unknown, maxBytes = 2000) => {
+    const s = typeof val === "string" ? val : String(val ?? "");
+    const enc = new TextEncoder();
+    if (enc.encode(s).length <= maxBytes) return s;
+    let lo = 0, hi = s.length;
+    while (lo < hi) {
+      const mid = (lo + hi) >> 1;
+      (enc.encode(s.slice(0, mid)).length <= maxBytes) ? lo = mid + 1 : hi = mid;
+    }
+    return s.slice(0, Math.max(0, lo - 1));
+  };

75-84: Use Object.entries to avoid inherited keys and TS ignore

for..in can traverse prototype properties; also avoids the TS ignore.

-    for (const key in params) {
-      // @ts-expect-error somehow typescript bugs
-      const value = params[key];
+    for (const [key, value] of Object.entries(params)) {
       if (value !== null && value !== undefined) {
-        values[key] = truncate(
-          typeof value !== "object" ? value : JSON.stringify(value),
-        );
+        values[key] = truncate(
+          typeof value !== "object" ? value : JSON.stringify(value),
+        );
       }
     }

97-101: Preconnect/dns-prefetch should target origin, not path; encode staticScript URL

Using /events path for preconnect/dns‑prefetch is suboptimal; also query param should be URL‑encoded.

 function Component({ collectorAddress, staticScriptUrl }: Props) {
   const collector = collectorAddress ?? DEFAULT_COLLECTOR_ADDRESS;
   const staticScript = staticScriptUrl ?? DEFAULT_ANALYTICS_SCRIPT_URL;
+  const collectorOrigin = (() => { try { return new URL(collector).origin; } catch { return collector; } })();

   return (
     <Head>
-      <link rel="dns-prefetch" href={collector} />
+      <link rel="dns-prefetch" href={collectorOrigin} />
       <link
         rel="preconnect"
-        href={collector}
+        href={collectorOrigin}
         crossOrigin="anonymous"
       />
       <script
         id="tracker"
         data-autocollect="false"
         data-hash-routing="true"
         data-url={collector}
         type="module"
-        src={`/live/invoke/website/loaders/analyticsScript.ts?url=${staticScript}`}
+        src={`/live/invoke/website/loaders/analyticsScript.ts?url=${encodeURIComponent(staticScript)}`}
       />

Also applies to: 109-109


16-28: Add Window.DECO typing

You declared Window.stonks but not Window.DECO, causing TS friction.

 declare global {
   interface Window {
     stonks: {
       event: (
         name: string,
         params: Record<string, string | boolean>,
       ) => void;
       view: (
         params: Record<string, string | boolean>,
       ) => void;
     };
+    DECO?: {
+      events: {
+        subscribe: (fn: (event: unknown) => unknown) => unknown;
+      };
+    };
   }
 }

35-35: Typo

“dimentions” → “dimensions”.

-  // Flags and additional dimentions
+  // Flags and additional dimensions

30-33: Exported trackerOriginal is unused here

If it’s intentional for external import, keep it. If not, remove to reduce bundle size.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ce899 and 10da010.

📒 Files selected for processing (5)
  • google-drive/utils/query.ts (1 hunks)
  • google-drive/utils/types.ts (3 hunks)
  • hubspot/actions/conversations/archiveThread.ts (0 hunks)
  • hubspot/actions/conversations/updateThread.ts (0 hunks)
  • website/components/OneDollarStats.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • hubspot/actions/conversations/updateThread.ts
  • hubspot/actions/conversations/archiveThread.ts
✅ Files skipped from review due to trivial changes (2)
  • google-drive/utils/query.ts
  • google-drive/utils/types.ts
🔇 Additional comments (1)
website/components/OneDollarStats.tsx (1)

37-47: Order-of-execution note

Ensure the module tracker script initializes window.stonks before oneDollarSnippet runs. You’re using two deferred scripts; order in DOM should preserve execution, but race is still possible on slow networks.

If races occur, gate trackPageview() calls until window.stonks is truthy, or buffer events.

Also applies to: 51-66

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
website/components/OneDollarStats.tsx (1)

51-65: Fix subscription invocation bug.

Line 65 ends with })(); which immediately invokes the return value of subscribe(). If subscribe() returns an unsubscribe function (common pattern), this would immediately unsubscribe the handler, preventing it from receiving any events. This is inconsistent with the second subscription at line 86, which correctly doesn't invoke the return value.

Apply this diff to fix the issue:

-  })();
+  });
🧹 Nitpick comments (2)
website/components/OneDollarStats.tsx (2)

48-49: Clarify the truncation limit.

The comment mentions a 2000 bytes limit, but the truncate function uses 990 characters. Consider documenting why this specific value was chosen (e.g., accounting for multiple fields, safety margin, etc.).


30-32: Remove unused export trackerOriginal.

The trackerOriginal constant is exported but never used anywhere in the codebase. Remove this dead code to reduce bundle size.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10da010 and dd97fc1.

📒 Files selected for processing (1)
  • website/components/OneDollarStats.tsx (1 hunks)
🔇 Additional comments (1)
website/components/OneDollarStats.tsx (1)

67-86: Event forwarding logic looks correct.

The subscription correctly filters and forwards non-"deco" events to window.stonks.event, merging accumulated props with event params. The truncation and serialization logic appropriately handles different value types.

Note: The @ts-expect-error on line 78 is acceptable given the dynamic nature of event parameters, though consider adding a more specific comment explaining why the type assertion is needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @website/components/Image.tsx:
- Around line 104-115: The optimizeMagento function builds query params
incorrectly when height is optional and when fit==='cover'; change it so you
only set the canvas param if height is present (e.g., check typeof height ===
"number") to avoid "400:undefined", and for fit avoid setting an empty
value—only add url.searchParams.set("fit", "bounds") when opts.fit !== "cover"
(omit the fit param entirely for cover). Update the body of optimizeMagento to
conditionally add canvas and fit params using those checks.
🧹 Nitpick comments (1)
website/components/Image.tsx (1)

126-129: Consider more robust URL pattern matching for Magento detection.

The substring check originalSrc.includes("media/catalog/product") at line 126 could produce false positives, matching any URL that contains this text in query parameters, fragments, or unrelated paths.

For more precise detection, consider checking the pathname specifically:

♻️ Proposed refactor
-    if (originalSrc.includes("media/catalog/product")) {
+    try {
+      const url = new URL(originalSrc);
+      if (url.pathname.includes("/media/catalog/product/")) {
+        return optimizeMagento(opts);
+      }
+    } catch {
+      // Invalid URL, fall through
+    }
+    
+    if (originalSrc.startsWith("https://cdn.vnda.")) {
       return optimizeMagento(opts);
     }
-    
-    if (originalSrc.startsWith("https://cdn.vnda.")) {

This checks the pathname component only and handles potential URL parsing errors gracefully.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd97fc1 and acab327.

📒 Files selected for processing (1)
  • website/components/Image.tsx

Comment on lines 104 to 115
const optimizeMagento = (opts: OptimizationOptions) => {
const { originalSrc, width, height } = opts;

const url = new URL(originalSrc);
url.searchParams.set("width", `${width}`);
url.searchParams.set("height", `${height}`);
url.searchParams.set("canvas", `${width}:${height}`);
url.searchParams.set("optimize", "low");
url.searchParams.set("fit", opts.fit === "cover" ? "" : "bounds");

return url.href;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle optional height parameter to prevent malformed URLs.

Line 110 constructs the canvas parameter using ${width}:${height}, but height is optional in the OptimizationOptions interface. When height is undefined, this will produce a malformed parameter like "400:undefined".

Additionally, line 112 sets the fit parameter to an empty string when opts.fit === "cover". This creates a query parameter with an empty value (fit=), which may not be the intended behavior—consider omitting the parameter entirely or using a different value.

🔧 Proposed fix
 const optimizeMagento = (opts: OptimizationOptions) => {
   const { originalSrc, width, height } = opts;
 
   const url = new URL(originalSrc);
   url.searchParams.set("width", `${width}`);
-  url.searchParams.set("height", `${height}`);
-  url.searchParams.set("canvas", `${width}:${height}`);
+  if (height) {
+    url.searchParams.set("height", `${height}`);
+    url.searchParams.set("canvas", `${width}:${height}`);
+  }
   url.searchParams.set("optimize", "low");
-  url.searchParams.set("fit", opts.fit === "cover" ? "" : "bounds");
+  if (opts.fit !== "cover") {
+    url.searchParams.set("fit", "bounds");
+  }
 
   return url.href;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const optimizeMagento = (opts: OptimizationOptions) => {
const { originalSrc, width, height } = opts;
const url = new URL(originalSrc);
url.searchParams.set("width", `${width}`);
url.searchParams.set("height", `${height}`);
url.searchParams.set("canvas", `${width}:${height}`);
url.searchParams.set("optimize", "low");
url.searchParams.set("fit", opts.fit === "cover" ? "" : "bounds");
return url.href;
};
const optimizeMagento = (opts: OptimizationOptions) => {
const { originalSrc, width, height } = opts;
const url = new URL(originalSrc);
url.searchParams.set("width", `${width}`);
if (height) {
url.searchParams.set("height", `${height}`);
url.searchParams.set("canvas", `${width}:${height}`);
}
url.searchParams.set("optimize", "low");
if (opts.fit !== "cover") {
url.searchParams.set("fit", "bounds");
}
return url.href;
};
🤖 Prompt for AI Agents
In @website/components/Image.tsx around lines 104 - 115, The optimizeMagento
function builds query params incorrectly when height is optional and when
fit==='cover'; change it so you only set the canvas param if height is present
(e.g., check typeof height === "number") to avoid "400:undefined", and for fit
avoid setting an empty value—only add url.searchParams.set("fit", "bounds") when
opts.fit !== "cover" (omit the fit param entirely for cover). Update the body of
optimizeMagento to conditionally add canvas and fit params using those checks.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
website/components/OneDollarStats.tsx (2)

65-65: ⚠️ Potential issue | 🔴 Critical

Critical bug: Immediately invoking the return value of subscribe unsubscribes the handler.

Line 65 ends with })(); — the trailing () immediately invokes the unsubscribe function returned by .subscribe(). According to the type definition in Events.tsx, subscribe returns () => void, which removes the event listener. This means the subscription is torn down immediately after being set up. Line 86 correctly uses }); without the invocation.

Remove the immediate function call:

Fix
-  })();
+  });

37-47: ⚠️ Potential issue | 🟠 Major

Remove the duplicate pushState and popstate patching on lines 39–46.

The trackerOriginal script already patches history.pushState and registers a popstate listener. The oneDollarSnippet repeats this patching on lines 39–46, causing both tracker implementations to fire on every SPA navigation. Although data-autocollect="false" is set (line 108), it only affects the auto-collection logic within the external tracker's a() function, not the hook installation itself—the pushState patch in trackerOriginal is installed unconditionally. Remove the duplicate patching here since the external tracker already handles route changes.

🤖 Fix all issues with AI agents
In `@website/components/OneDollarStats.tsx`:
- Around line 30-32: Remove the dead exported constant trackerOriginal from the
OneDollarStats component: locate the trackerOriginal declaration and its export
in OneDollarStats.tsx and delete the entire const definition and export (no
other files reference it), ensuring you do not remove any other exports or
functions such as the runtime tracker code; run a project-wide search for
"trackerOriginal" to confirm no usages remain and commit the cleanup.

Comment on lines +30 to +32
export const trackerOriginal = `(function() {
"use strict";function g(t){let e={};return["utm_campaign","utm_source","utm_medium","utm_term","utm_content"].forEach(n=>{let o=t.get(n);o&&(e[n]=o)}),e}function p(t){if(!t)return;let e=t.split(";"),n={};for(let o of e){let r=o.split("=").map(s=>s.trim());r.length!==2||r[0]===""||r[1]===""||(n[r[0]]=r[1])}return Object.keys(n).length===0?void 0:n}window.stonks={event:y,view:S};var l=document.currentScript,h=l?.getAttribute("data-hash-routing")!==null,m={isLocalhost:/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname)||location.protocol==="file:",isHeadlessBrowser:!!(window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress)};async function w(t){let e=l?.getAttribute("data-url")||"https://collector.onedollarstats.com/events",n=new URL(location.href);n.search="","path"in t&&t.path&&(n.pathname=t.path);let o=n.href.replace(/\\/$/,""),r=t.referrer??void 0;if(!r){let i=new URL(location.href),c=document.referrer&&document.referrer!=="null"?document.referrer:void 0;if(c){let u=new URL(c);u.hostname!==i.hostname&&(r=u.href)}}let s={u:o,e:[{t:t.type,h,r,p:t.props}]};t.utm&&Object.keys(t.utm).length>0&&(s.qs=t.utm),!(navigator.sendBeacon!==void 0&&navigator.sendBeacon(e,JSON.stringify(s)))&&fetch(e,{body:JSON.stringify(s),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(i=>console.error("fetch() failed:"))}async function y(t,e,n){if(d())return;let o={};typeof e=="string"?(o.path=e,n&&(o.props=n)):typeof e=="object"&&(o.props=e);let r=o?.path||void 0;if(!r){let s=document.body?.getAttribute("data-s:path")||document.querySelector('meta[name="stonks-path"]')?.getAttribute("content");s&&(r=s)}w({type:t,props:o?.props,path:r})}function A(t){if(t.type==="auxclick"&&t.button!==1)return;let e=t.target,n=e.getAttribute("data-s:event");if(!n)return;let o=e.getAttribute("data-s:event-props"),r=o?p(o):void 0,s=e.getAttribute("data-s:event-path")||void 0;y(n,s,r)}async function S(t,e){let n={};typeof t=="string"?(n.path=t,e&&(n.props=e)):typeof t=="object"&&(n.props=t),b({path:n?.path,props:n?.props},!1)}async function b(t,e=!0){if(e&&d())return;let n=new URLSearchParams(location.search),o=g(n),r=t?.path||void 0;if(!r){let i=document.body?.getAttribute("data-s:path")||document.querySelector('meta[name="stonks-path"]')?.getAttribute("content");i&&(r=i)}let s=t.props||void 0;if(!s){let i=l?.getAttribute("data-props"),c=i?p(i)||{}:{},u=document.querySelectorAll("[data-s\\:view-props]");for(let v of Array.from(u)){let f=v.getAttribute("data-s:view-props");if(!f)continue;let P=p(f);Object.assign(c,P)}s=c}w({type:"PageView",props:Object.keys(s).length>0?s:void 0,path:r,utm:o})}async function a(){let t=document.querySelector('meta[name="stonks-collect"]')?.getAttribute("content"),e=document.body?.getAttribute("data-s:collect");if(t==="false"||e==="false"){a.lastPage=null;return}if(!(l?.getAttribute("data-autocollect")!=="false")&&t!=="true"&&e!=="true"){a.lastPage=null;return}if(!h&&a.lastPage===location.pathname){console.warn("Ignoring event PageView - pathname has not changed");return}if(d())return;a.lastPage=location.pathname;let o=l?.getAttribute("data-props"),r=o?p(o)||{}:{},s=document.querySelectorAll("[data-s\\:view-props]");for(let i of Array.from(s)){let c=i.getAttribute("data-s:view-props");if(!c)continue;let u=p(c);Object.assign(r,u)}b({props:Object.keys(r).length>0?r:void 0},!1)}(e=>e.lastPage=null)(a||={});function d(){return!!(m.isLocalhost&&l?.getAttribute("data-allow-localhost")!=="true"||m.isHeadlessBrowser)}if(window.history.pushState){let t=window.history.pushState;window.history.pushState=function(e,n,o){t.apply(this,[e,n,o]),window.requestAnimationFrame(()=>{a()})},window.addEventListener("popstate",()=>{window.requestAnimationFrame(()=>{a()})})}document.visibilityState!=="visible"?document.addEventListener("visibilitychange",()=>{!a.lastPage&&document.visibilityState==="visible"&&a()}):a();document.addEventListener("click",A);
})();`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'trackerOriginal' --type-add 'tsx:*.tsx' --type=ts --type=tsx

Repository: deco-cx/apps

Length of output: 142


Remove unused trackerOriginal export.

The constant trackerOriginal is exported at line 30 but is not referenced anywhere in the codebase. Remove it as dead code.

🤖 Prompt for AI Agents
In `@website/components/OneDollarStats.tsx` around lines 30 - 32, Remove the dead
exported constant trackerOriginal from the OneDollarStats component: locate the
trackerOriginal declaration and its export in OneDollarStats.tsx and delete the
entire const definition and export (no other files reference it), ensuring you
do not remove any other exports or functions such as the runtime tracker code;
run a project-wide search for "trackerOriginal" to confirm no usages remain and
commit the cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants