@liiift-studio/sanity-visitor-insights 0.42.0 → 0.43.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": "@liiift-studio/sanity-visitor-insights",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Visitor-behaviour analytics for type-foundry Sanity Studios — GA4, Vercel and order data reconciled honestly",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1960,3 +1960,126 @@ describe('a multi-family order splits its total rather than counting it whole',
1960
1960
  })
1961
1961
  })
1962
1962
  })
1963
+
1964
+ describe('behaviours whose comments explain them but whose tests did not', () => {
1965
+ it('takes the largest consent row, never their sum', async () => {
1966
+ // Distinct-user counts across several event rows overlap: anyone who fired two of them is in
1967
+ // both. Summing would exceed the true union and can exceed the user count entirely, which
1968
+ // then trips the "grants exceed users" error on healthy data. The existing test used one row,
1969
+ // so max and sum were indistinguishable.
1970
+ const data = await measurementHealth({
1971
+ config: siteConfig(),
1972
+ range,
1973
+ ga4: createFakeGa4Client({
1974
+ batch: () => [
1975
+ makeGa4Total(400),
1976
+ makeGa4Report([{ metrics: [400, 320] }]),
1977
+ // Three consent event rows, overlapping: 240 is the tightest correct lower bound.
1978
+ makeGa4Report([{ metrics: [240] }, { metrics: [180] }, { metrics: [120] }]),
1979
+ ],
1980
+ }),
1981
+ vercel: createFakeVercelClient(makeVercelPageviews({ '2026-08-20': 1000 })),
1982
+ sanity: null,
1983
+ })
1984
+
1985
+ // 240 of 320 users = 75%. The sum, 540, would exceed 320 and report a source error.
1986
+ expect(data.consentRate).toEqual({ status: 'ok', value: 75 })
1987
+ })
1988
+
1989
+ it('withholds the settled shortfall when too few days can be compared', async () => {
1990
+ // A ratio built from one or two shared days is noise wearing a decimal point; the whole-range
1991
+ // totals at least cover one consistent window each.
1992
+ const data = await measurementHealth({
1993
+ config: siteConfig(),
1994
+ range,
1995
+ now: new Date('2026-08-26T12:00:00Z'),
1996
+ ga4: createFakeGa4Client({
1997
+ batch: () => [
1998
+ makeGa4Total(400),
1999
+ makeGa4Report([{ metrics: [400, 320] }]),
2000
+ makeGa4Total(0),
2001
+ makeGa4Report([{ dimensions: ['20260820'], metrics: [80] }, { dimensions: ['20260821'], metrics: [80] }]),
2002
+ ],
2003
+ }),
2004
+ // Vercel covers enough of the week to count as a daily series — which is what makes this
2005
+ // reachable — but GA4 reported only two of those days, so just two can be compared.
2006
+ vercel: createFakeVercelClient(makeVercelPageviews({
2007
+ '2026-08-20': 100, '2026-08-21': 100, '2026-08-22': 100,
2008
+ '2026-08-23': 100, '2026-08-24': 100, '2026-08-25': 100,
2009
+ })),
2010
+ sanity: null,
2011
+ })
2012
+
2013
+ // Two shared days would give (200-160)/200 = 0.2. Below the floor it falls back to the
2014
+ // whole-range totals instead, which at least cover one consistent window each.
2015
+ expect(data.shortfallRatio).not.toBeCloseTo(0.2, 5)
2016
+ })
2017
+ })
2018
+
2019
+ describe('an absent GA4 total is absent, not zero', () => {
2020
+ it('leaves metricTotal undefined when the response carried none', async () => {
2021
+ // toMetricNumber yields NaN for an absent value. Letting that through would put NaN into a
2022
+ // share — a silent wrong answer — while `undefined` means "not requested" and every caller
2023
+ // already withholds the share rather than dividing by a subtotal.
2024
+ const bodies: unknown[] = []
2025
+ globalThis.fetch = (async () => ({
2026
+ ok: true, status: 200,
2027
+ async json() {
2028
+ bodies.push(1)
2029
+ // A response with rows but no totals block at all.
2030
+ return { rows: [{ dimensionValues: [{ value: 'x' }], metricValues: [{ value: '5' }] }] }
2031
+ },
2032
+ })) as unknown as typeof fetch
2033
+
2034
+ try {
2035
+ const client = createGa4Client('1', { client_email: 'x', private_key: 'y' } as never)
2036
+ const report = await client.runReport({
2037
+ metrics: [{ name: 'sessions' }],
2038
+ dateRanges: [{ startDate: '2026-08-20', endDate: '2026-08-26' }],
2039
+ })
2040
+ expect(report.metricTotal).toBeUndefined()
2041
+ expect(Number.isNaN(report.metricTotal as number)).toBe(false)
2042
+ } finally {
2043
+ bodies.length = 0
2044
+ }
2045
+ })
2046
+ })
2047
+
2048
+ describe('the revenue split refuses what it cannot check', () => {
2049
+ const sourceRow = (source: string, sessions: number) => ({
2050
+ dimensions: [source, 'Referral', 'referral', '(not set)'],
2051
+ metrics: [sessions, sessions],
2052
+ })
2053
+
2054
+ const run = (actuals: { revenue: number | null; orders: number | null } | null, purchases: number) =>
2055
+ acquisition({
2056
+ config: siteConfig(),
2057
+ range,
2058
+ actuals,
2059
+ ga4: createFakeGa4Client({
2060
+ single: (request) => (request.metrics?.some((m) => m.name === 'ecommercePurchases')
2061
+ ? makeGa4Report([{ dimensions: ['a.test', 'Referral', 'referral', '(not set)'], metrics: [purchases, 1000] }])
2062
+ : makeGa4Report([sourceRow('a.test', 300)])),
2063
+ }),
2064
+ notices: [],
2065
+ })
2066
+
2067
+ it('withholds the split when the order book could not be read', async () => {
2068
+ // Unknown coverage was treated as PASSING, so an unreadable order book switched off the
2069
+ // window that catches over-attribution and the split rendered as sound on a double-firing tag.
2070
+ const data = await run(null, 20)
2071
+ expect(data.splitIsSound).toBe(false)
2072
+ for (const row of data.rows) expect(row.revenueShare ?? null).toBeNull()
2073
+ })
2074
+
2075
+ it('withholds it when GA4 attributed more purchases than there are orders', async () => {
2076
+ // The upper bound: counting sales that did not happen is a finding, not a shortage.
2077
+ const data = await run({ revenue: 5000, orders: 7 }, 20)
2078
+ expect(data.splitIsSound).toBe(false)
2079
+ })
2080
+
2081
+ it('computes it when coverage sits inside the window', async () => {
2082
+ const data = await run({ revenue: 5000, orders: 10 }, 6)
2083
+ expect(data.splitIsSound).toBe(true)
2084
+ })
2085
+ })