@decocms/apps-website 7.54.0 → 7.55.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps-website",
3
- "version": "7.54.0",
3
+ "version": "7.55.0",
4
4
  "type": "module",
5
5
  "description": "Deco generic-site app: SEO, analytics, theme, and content utilities shared across every commerce backend",
6
6
  "repository": {
@@ -32,8 +32,8 @@
32
32
  "lint:unused": "knip"
33
33
  },
34
34
  "dependencies": {
35
- "@decocms/blocks": "7.54.0",
36
- "@decocms/apps-commerce": "7.54.0"
35
+ "@decocms/blocks": "7.55.0",
36
+ "@decocms/apps-commerce": "7.55.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": "^19.0.0",
@@ -1,127 +1,16 @@
1
1
  /**
2
- * Stats — deco's first-party analytics collector.
2
+ * Moved to `@decocms/blocks/hooks`, and re-exported here so nothing that already imports
3
+ * `@decocms/apps-website/components/Stats` breaks.
3
4
  *
4
- * Mount once in `__root.tsx`, alongside or instead of `<OneDollarStats />`:
5
+ * It moved because `DecoRootLayout` mounts it now: `@decocms/tanstack` importing from an apps
6
+ * package would add an edge to a dependency graph this repo keeps one-way on purpose, and the
7
+ * component never needed to be here — it is a `<link>` and a `<script>` pointing at our own
8
+ * collector, not a vendor integration like `OneDollarStats`.
5
9
  *
6
- * ```tsx
7
- * <DecoRootLayout … >
8
- * <Stats />
9
- * </DecoRootLayout>
10
- * ```
11
- *
12
- * ## Why this is twenty lines and OneDollarStats is three hundred
13
- *
14
- * Not because it does less — because the work is on the other side. The lilstts SDK
15
- * has no notion of SPA navigation the way this app routes, no notion of the
16
- * `deco_segment` cookie, and no notion of `window.DECO.events`, so the component has
17
- * to wrap `history.pushState`, poll for globals, read and decode the cookie, and
18
- * forward every commerce event by hand.
19
- *
20
- * The deco collector's own bundle already does all of it, and is tested doing it:
21
- * the core module takes the first pageview through the prerender guard, wraps
22
- * `pushState`, `replaceState` and `popstate`, and flushes on `pagehide` and
23
- * `visibilitychange`; the deco module reads `deco_segment` into experiment
24
- * assignments and subscribes to `window.DECO.events`, mapping the commerce
25
- * vocabulary. None of that belongs in a component that would then be a second
26
- * implementation of it, drifting from the first.
27
- *
28
- * So there is no `useEffect` here, and that is the point. Nothing to hydrate, no
29
- * readiness polling, no module-level guard against StrictMode double-mounting —
30
- * because there is no client state to guard.
31
- *
32
- * ## data- attributes, not a global
33
- *
34
- * `dev` and `debug` are read off the tag rather than from `window.__dq`, and this is
35
- * load-bearing on exactly this framework. TanStack hoists `<script async>` into
36
- * `<head>` ABOVE any inline configuration block — measured at byte 190 against byte
37
- * 1108 on a real site. A component that set a global and expected the collector to
38
- * find it would boot into silence here, with no error: the collector would see a
39
- * development host, skip, and say nothing. Attributes cannot lose that race because
40
- * they are on the element that is executing.
41
- *
42
- * ## Off by default
43
- *
44
- * `DECO_ANALYTICS_ENABLED` must be set to `true`. This is the inverse of
45
- * `ONEDOLLAR_ENABLED`, which defaults to on, and the asymmetry is deliberate: one is
46
- * the incumbent and the other is being introduced. The two gates are also
47
- * independent, so a site can run both during a shadow comparison and neither gate
48
- * can turn the other off.
49
- */
50
-
51
- export interface Props {
52
- /**
53
- * Where the collector is published. Empty means same-origin, which is the
54
- * intended deployment: the script and the beacon are served from the site's own
55
- * hostname so no third-party request is involved and nothing is blocked.
56
- */
57
- origin?: string;
58
- /**
59
- * The site's public key, for sites NOT served through our CDN.
60
- *
61
- * Sites behind our edge are identified by the `Host` header, which a visitor
62
- * cannot forge; those must leave this unset. A key travels in the page source
63
- * where anyone can read and reuse it, so a key-identified site is recorded with
64
- * `site_id_source = tag` and is never billed from.
65
- */
66
- siteKey?: string;
67
- /** `defer` instead of `async`. Only for a page that needs strict ordering. */
68
- defer?: boolean;
69
- /**
70
- * Collect from localhost. The collector refuses local and private hostnames by
71
- * default, which is why a developer sees nothing until this is on.
72
- */
73
- dev?: boolean;
74
- /** Log every queued and flushed batch to the console. */
75
- debug?: boolean;
76
- }
77
-
78
- /** Same-origin. See {@link Props.origin}. */
79
- export const DEFAULT_ORIGIN = "";
80
-
81
- /**
82
- * Opt-in, and independent of `ONEDOLLAR_ENABLED` so both can run at once.
10
+ * Sites do not need to import it at all any more. Set `DECO_ANALYTICS_ENABLED=true`.
83
11
  */
84
- const DECO_ANALYTICS_ENABLED = process.env.DECO_ANALYTICS_ENABLED === "true";
85
- const DECO_ANALYTICS_ORIGIN = process.env.DECO_ANALYTICS_ORIGIN;
86
- const DECO_ANALYTICS_SITE_KEY = process.env.DECO_ANALYTICS_SITE_KEY;
87
-
88
- function Stats({ origin, siteKey, defer, dev, debug }: Props) {
89
- if (!DECO_ANALYTICS_ENABLED) return null;
90
-
91
- const base = origin ?? DECO_ANALYTICS_ORIGIN ?? DEFAULT_ORIGIN;
92
- const key = siteKey ?? DECO_ANALYTICS_SITE_KEY;
93
-
94
- return (
95
- <>
96
- {/*
97
- * Only when the collector is on another origin. A `preconnect` to the page's
98
- * own origin is a wasted hint at best, and on some browsers it is a second
99
- * connection opened for nothing.
100
- */}
101
- {base ? <link rel="preconnect" href={base} crossOrigin="anonymous" /> : null}
102
- <script
103
- id="deco-analytics"
104
- async={!defer}
105
- defer={defer}
106
- // THE KEY GOES IN THE URL, not in a `data-` attribute. The collector resolves the
107
- // site server-side while RENDERING the bundle -- it reads `?k=` and writes the
108
- // resolved config into the script it returns -- so a key on the element arrives
109
- // far too late to matter. It is also never read: the bundle only looks at
110
- // `data-dev` and `data-debug`.
111
- //
112
- // This was `data-site` and it would have failed the way this project's failures
113
- // always do: the collector resolves nothing, serves the `s:"unknown"` fallback,
114
- // and the site collects exactly zero with no error anywhere. Same shape as the
115
- // bug that once made the entire self-serve tier silent.
116
- src={`${base}/_dq/a.js${key ? `?k=${encodeURIComponent(key)}` : ""}`}
117
- // Rendered only when true. `data-dev="false"` and an absent attribute mean
118
- // the same thing to the collector, and the absent one cannot be mistaken
119
- // for a deliberate setting by someone reading the page source.
120
- data-dev={dev ? "true" : undefined}
121
- data-debug={debug ? "true" : undefined}
122
- />
123
- </>
124
- );
125
- }
126
-
127
- export default Stats;
12
+ export {
13
+ Stats as default,
14
+ STATS_DEFAULT_ORIGIN as DEFAULT_ORIGIN,
15
+ type StatsProps as Props,
16
+ } from "@decocms/blocks/hooks";
@@ -1,104 +0,0 @@
1
- // The gate and the tag. There is no client behaviour to test — the collector's bundle owns
2
- // pageviews, SPA navigation and DECO events, and it is tested where it lives. What can break
3
- // here is what this component actually decides: whether to render at all, where it points, and
4
- // which attributes it emits.
5
- //
6
- // `renderToStaticMarkup` rather than a DOM render: the component has no effects and no state, so
7
- // mounting it would test React rather than this file.
8
- import { afterEach, describe, expect, it, vi } from "vitest";
9
-
10
- const ENV = { ...process.env };
11
-
12
- afterEach(() => {
13
- process.env = { ...ENV };
14
- vi.resetModules();
15
- });
16
-
17
- /** Re-imported per test, because the gate is read at MODULE LOAD. A test that sets the variable
18
- * after importing would be asserting against the value the previous test left behind — and it
19
- * would pass or fail depending on file order, which is the worst kind of green. */
20
- async function render(props: Record<string, unknown> = {}) {
21
- const { renderToStaticMarkup } = await import("react-dom/server");
22
- const { default: Stats } = await import("./Stats");
23
- const { createElement } = await import("react");
24
- return renderToStaticMarkup(createElement(Stats, props));
25
- }
26
-
27
- describe("Stats", () => {
28
- it("renders nothing unless explicitly enabled", async () => {
29
- delete process.env.DECO_ANALYTICS_ENABLED;
30
- expect(await render()).toBe("");
31
-
32
- // Not "any truthy value". `ONEDOLLAR_ENABLED` defaults to ON and is disabled with
33
- // "false"; this one defaults to OFF and needs "true". A loose check here would make
34
- // `DECO_ANALYTICS_ENABLED=0` turn analytics on, which is the opposite of what anyone
35
- // setting it to 0 intends.
36
- process.env.DECO_ANALYTICS_ENABLED = "1";
37
- vi.resetModules();
38
- expect(await render()).toBe("");
39
- });
40
-
41
- it("points at the same origin by default, and preconnects only when it does not", async () => {
42
- process.env.DECO_ANALYTICS_ENABLED = "true";
43
- const same = await render();
44
- expect(same).toContain('src="/_dq/a.js"');
45
- // A preconnect to the page's own origin is a wasted hint, and on some browsers a
46
- // second connection opened for nothing.
47
- expect(same).not.toContain("preconnect");
48
-
49
- vi.resetModules();
50
- process.env.DECO_ANALYTICS_ORIGIN = "https://analytics.example.com";
51
- const cross = await render();
52
- expect(cross).toContain('src="https://analytics.example.com/_dq/a.js"');
53
- expect(cross).toContain("preconnect");
54
- });
55
-
56
- it("carries dev and debug as attributes, and omits them when off", async () => {
57
- process.env.DECO_ANALYTICS_ENABLED = "true";
58
- // The whole reason these are attributes: TanStack hoists `<script async>` into `<head>`
59
- // above any inline config block, so a global set alongside the tag loses the race and the
60
- // collector boots into silence with no error.
61
- const on = await render({ dev: true, debug: true });
62
- expect(on).toContain('data-dev="true"');
63
- expect(on).toContain('data-debug="true"');
64
-
65
- vi.resetModules();
66
- const off = await render();
67
- // Absent, not `="false"`. The collector treats them the same; a reader of the page source
68
- // does not, and `data-dev="false"` looks like someone decided something.
69
- expect(off).not.toContain("data-dev");
70
- expect(off).not.toContain("data-debug");
71
- });
72
-
73
- it("puts the site key in the URL, because that is where the collector reads it", async () => {
74
- process.env.DECO_ANALYTICS_ENABLED = "true";
75
- // Sites behind our edge are identified by the Host header, which a visitor cannot forge.
76
- // Emitting an empty key would put a `tag`-sourced identity on a site that has a
77
- // trustworthy one, and `tag` is the source that must never reach an invoice.
78
- const none = await render();
79
- expect(none).toContain('src="/_dq/a.js"');
80
- expect(none).not.toContain("?k=");
81
-
82
- vi.resetModules();
83
- process.env.DECO_ANALYTICS_SITE_KEY = "dq_abc123";
84
- const keyed = await render();
85
- // IN THE QUERY STRING. The collector resolves the site while rendering the bundle, from
86
- // `?k=` -- a key on the element is read by nothing and arrives after the decision. As
87
- // `data-site` this rendered fine, resolved nothing, served the `s:"unknown"` fallback and
88
- // collected zero without an error anywhere.
89
- expect(keyed).toContain("/_dq/a.js?k=dq_abc123");
90
- expect(keyed).not.toContain("data-site");
91
- });
92
-
93
- it("uses defer only when asked, async otherwise", async () => {
94
- process.env.DECO_ANALYTICS_ENABLED = "true";
95
- // Nothing visual may depend on this script. `async` is what keeps a slow or failed
96
- // collector from becoming a slow or broken page.
97
- expect(await render()).toContain("async");
98
-
99
- vi.resetModules();
100
- const deferred = await render({ defer: true });
101
- expect(deferred).toContain("defer");
102
- expect(deferred).not.toContain("async");
103
- });
104
- });