@faststore/core 4.5.1 → 4.6.0-dev.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/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +130 -90
- package/CHANGELOG.md +19 -17
- package/README.md +1 -1
- package/cypress/integration/seo.test.js +35 -5
- package/discovery.config.default.js +1 -2
- package/next-seo.config.ts +3 -0
- package/next.config.js +21 -5
- package/package.json +8 -8
- package/src/components/product/ProductGrid/ProductGrid.tsx +0 -1
- package/src/components/skeletons/ProductGridSkeleton/ProductGridSkeleton.tsx +1 -4
- package/src/components/templates/SearchPage/SearchWrapper.tsx +5 -15
- package/src/components/ui/ProductGallery/ProductGallery.tsx +24 -69
- package/src/instrumentation.ts +15 -7
- package/src/pages/api/graphql.ts +37 -11
- package/src/sdk/ui/useScrollRestoration.tsx +32 -616
- package/src/server/index.ts +38 -10
- package/src/server/options.ts +3 -22
- package/src/styles/global/index.scss +0 -12
- package/test/instrumentation.test.ts +27 -0
- package/test/pages/api/graphql.test.ts +40 -2
- package/test/server/rootSpan.test.ts +90 -0
- package/src/components/ui/ProductGallery/GalleryPageHeightLock.tsx +0 -238
- package/test/components/skeletons/ProductGridSkeleton.browser.test.tsx +0 -32
- package/test/components/templates/SearchPage/SearchWrapper.browser.test.tsx +0 -137
- package/test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx +0 -230
- package/test/sdk/ui/useScrollRestoration.browser.test.ts +0 -798
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0-dev.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"style-loader": "^3.3.1",
|
|
72
72
|
"swr": "^2.2.5",
|
|
73
73
|
"use-sync-external-store": "^1.6.0",
|
|
74
|
-
"@faststore/
|
|
75
|
-
"@faststore/
|
|
76
|
-
"@faststore/
|
|
77
|
-
"@faststore/sdk": "4.
|
|
78
|
-
"@faststore/
|
|
74
|
+
"@faststore/api": "4.6.0-dev.0",
|
|
75
|
+
"@faststore/diagnostics": "4.6.0-dev.0",
|
|
76
|
+
"@faststore/lighthouse": "4.6.0-dev.0",
|
|
77
|
+
"@faststore/sdk": "4.6.0-dev.0",
|
|
78
|
+
"@faststore/ui": "4.6.0-dev.0"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@cypress/code-coverage": "^3.12.1",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"@types/fs-extra": "^9.0.13",
|
|
89
89
|
"@types/node": "20.19.13",
|
|
90
90
|
"@vitejs/plugin-react": "^5.1.2",
|
|
91
|
-
"@vitest/coverage-v8": "4.0.
|
|
91
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
92
92
|
"@vtex/prettier-config": "1.0.0",
|
|
93
93
|
"axe-core": "^4.8.1",
|
|
94
94
|
"cypress": "12.17.4",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"is-ci": "^3.0.0",
|
|
100
100
|
"jsdom": "^27.4.0",
|
|
101
101
|
"typescript": "5.9.3",
|
|
102
|
-
"vitest": "4.0.
|
|
102
|
+
"vitest": "4.0.18"
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|
|
105
105
|
"graphql": "16.11.0",
|
|
@@ -7,19 +7,16 @@ import ProductCardSkeleton from '../ProductCardSkeleton'
|
|
|
7
7
|
interface ProductGridSkeletonProps {
|
|
8
8
|
loading?: boolean
|
|
9
9
|
aspectRatio?: number
|
|
10
|
-
/** How many placeholder cards to render — defaults to the global ITEMS_PER_PAGE. */
|
|
11
|
-
count?: number
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
function ProductGridSkeleton({
|
|
15
13
|
children,
|
|
16
14
|
aspectRatio,
|
|
17
15
|
loading = true,
|
|
18
|
-
count = ITEMS_PER_PAGE,
|
|
19
16
|
}: PropsWithChildren<ProductGridSkeletonProps>) {
|
|
20
17
|
return loading ? (
|
|
21
18
|
<ul data-fs-product-grid>
|
|
22
|
-
{Array.from({ length:
|
|
19
|
+
{Array.from({ length: ITEMS_PER_PAGE }, (_, index) => (
|
|
23
20
|
<li key={String(index)}>
|
|
24
21
|
<ProductCardSkeleton aspectRatio={aspectRatio} bordered />
|
|
25
22
|
</li>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
2
1
|
import { useSearch } from '@faststore/sdk'
|
|
3
2
|
import { useRouter } from 'next/router'
|
|
4
3
|
|
|
@@ -41,19 +40,6 @@ export default function SearchWrapper({
|
|
|
41
40
|
selectedFacets,
|
|
42
41
|
})
|
|
43
42
|
|
|
44
|
-
const redirectUrl = pageProductGalleryData?.redirect?.url
|
|
45
|
-
const lastRedirectedUrl = useRef<string | null>(null)
|
|
46
|
-
|
|
47
|
-
// Intelligent Search redirects must run in an effect — calling router.replace
|
|
48
|
-
// during render re-fires on every state update (e.g. scrollRestoration's
|
|
49
|
-
// resetInfiniteScroll) and loops until the tab hangs.
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!redirectUrl || lastRedirectedUrl.current === redirectUrl) return
|
|
52
|
-
|
|
53
|
-
lastRedirectedUrl.current = redirectUrl
|
|
54
|
-
void router.replace(redirectUrl)
|
|
55
|
-
}, [redirectUrl, router])
|
|
56
|
-
|
|
57
43
|
const emptySearchProps = storeConfig.experimental.enableSearchSSR
|
|
58
44
|
? {
|
|
59
45
|
title: storeConfig.seo.search.bodyH1 ?? 'Showing results for:',
|
|
@@ -72,7 +58,11 @@ export default function SearchWrapper({
|
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
// Redirect when there are registered Intelligent Search redirects on VTEX Admin
|
|
75
|
-
if (
|
|
61
|
+
if (pageProductGalleryData?.redirect?.url) {
|
|
62
|
+
router.replace(pageProductGalleryData?.redirect?.url, null, {
|
|
63
|
+
shallow: true,
|
|
64
|
+
})
|
|
65
|
+
|
|
76
66
|
return (
|
|
77
67
|
<PageProvider context={{ globalSettings }}>
|
|
78
68
|
<RenderSections globalSections={globalSections}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextSeo } from 'next-seo'
|
|
2
2
|
import dynamic from 'next/dynamic'
|
|
3
|
-
import { Suspense,
|
|
3
|
+
import { Suspense, useState, type MouseEvent } from 'react'
|
|
4
4
|
|
|
5
5
|
import { useSearch } from '@faststore/sdk'
|
|
6
6
|
import { useUI } from '@faststore/ui'
|
|
@@ -24,10 +24,6 @@ import useScreenResize from 'src/sdk/ui/useScreenResize'
|
|
|
24
24
|
|
|
25
25
|
import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice'
|
|
26
26
|
import styles from '../../sections/ProductGallery/section.module.scss'
|
|
27
|
-
import GalleryPageHeightLock, {
|
|
28
|
-
getGalleryViewportBucket,
|
|
29
|
-
resolveReservedGalleryHeight,
|
|
30
|
-
} from './GalleryPageHeightLock'
|
|
31
27
|
import ProductGalleryPage from './ProductGalleryPage'
|
|
32
28
|
const FilterSkeleton = dynamic(
|
|
33
29
|
() =>
|
|
@@ -37,6 +33,8 @@ const FilterSkeleton = dynamic(
|
|
|
37
33
|
)
|
|
38
34
|
)
|
|
39
35
|
|
|
36
|
+
const GalleryPageSkeleton = <ProductGridSkeleton loading />
|
|
37
|
+
|
|
40
38
|
export interface ProductGalleryProps {
|
|
41
39
|
title?: string
|
|
42
40
|
searchTerm?: string
|
|
@@ -133,13 +131,7 @@ function ProductGallery({
|
|
|
133
131
|
} = useOverrideComponents<'ProductGallery'>()
|
|
134
132
|
|
|
135
133
|
const { openFilter, filter: displayFilter } = useUI()
|
|
136
|
-
const {
|
|
137
|
-
pages,
|
|
138
|
-
addNextPage,
|
|
139
|
-
addPrevPage,
|
|
140
|
-
itemsPerPage,
|
|
141
|
-
state: { term, sort, selectedFacets },
|
|
142
|
-
} = useSearch()
|
|
134
|
+
const { pages, addNextPage, addPrevPage, itemsPerPage } = useSearch()
|
|
143
135
|
const context = usePage<SearchPageContext | PLPContext>()
|
|
144
136
|
const data = context?.data
|
|
145
137
|
const facets = useDelayedFacets(data) ?? []
|
|
@@ -159,25 +151,6 @@ function ProductGallery({
|
|
|
159
151
|
(data as PLPContext['data'])?.collection?.meta?.selectedFacets ?? []
|
|
160
152
|
const filter = useFilter(facets, initialSelectedFacets)
|
|
161
153
|
|
|
162
|
-
// Keep the results column tall on PDP→PLP back nav before pages remount.
|
|
163
|
-
// Only while products are loading — once grids paint, natural height wins
|
|
164
|
-
// (avoids huge gaps after a viewport resize with a stale reserved minHeight).
|
|
165
|
-
// Populate after mount so SSR/hydration always start at 0 (sessionStorage is
|
|
166
|
-
// client-only and would otherwise mismatch).
|
|
167
|
-
const [reservedGalleryHeight, setReservedGalleryHeight] = useState(0)
|
|
168
|
-
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
setReservedGalleryHeight(
|
|
171
|
-
resolveReservedGalleryHeight(hasProductsLoaded, pages, {
|
|
172
|
-
path: globalThis.location.pathname,
|
|
173
|
-
term: term ?? null,
|
|
174
|
-
sort: sort ?? null,
|
|
175
|
-
selectedFacets,
|
|
176
|
-
viewport: getGalleryViewportBucket(),
|
|
177
|
-
})
|
|
178
|
-
)
|
|
179
|
-
}, [pages, term, sort, selectedFacets, hasProductsLoaded])
|
|
180
|
-
|
|
181
154
|
return (
|
|
182
155
|
<section data-testid="product-gallery" data-fs-product-listing>
|
|
183
156
|
{searchTerm && (
|
|
@@ -299,14 +272,7 @@ function ProductGallery({
|
|
|
299
272
|
)}
|
|
300
273
|
</FilterButtonSkeleton.Component>
|
|
301
274
|
</div>
|
|
302
|
-
<div
|
|
303
|
-
data-fs-product-listing-results
|
|
304
|
-
style={
|
|
305
|
-
reservedGalleryHeight > 0
|
|
306
|
-
? { minHeight: reservedGalleryHeight }
|
|
307
|
-
: undefined
|
|
308
|
-
}
|
|
309
|
-
>
|
|
275
|
+
<div data-fs-product-listing-results>
|
|
310
276
|
{/* Add link to previous page. This helps on SEO */}
|
|
311
277
|
{!!prev && (
|
|
312
278
|
<div data-fs-product-listing-pagination="top">
|
|
@@ -347,36 +313,25 @@ function ProductGallery({
|
|
|
347
313
|
</LinkButtonPrev.Component>
|
|
348
314
|
</div>
|
|
349
315
|
)}
|
|
350
|
-
{/* Render ALL products
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
itemsPerPage={itemsPerPage}
|
|
370
|
-
firstPage={pages[0]}
|
|
371
|
-
shouldShowComparison={showComparisonProducts}
|
|
372
|
-
compareLabel={productComparison?.labels?.compareButton}
|
|
373
|
-
/>
|
|
374
|
-
</Suspense>
|
|
375
|
-
) : (
|
|
376
|
-
<ProductGridSkeleton loading count={itemsPerPage} />
|
|
377
|
-
)}
|
|
378
|
-
</GalleryPageHeightLock>
|
|
379
|
-
))}
|
|
316
|
+
{/* Render ALL products */}
|
|
317
|
+
{hasProductsLoaded ? (
|
|
318
|
+
<Suspense fallback={GalleryPageSkeleton}>
|
|
319
|
+
{pages.map((page) => (
|
|
320
|
+
<ProductGalleryPage
|
|
321
|
+
key={`gallery-page-${page}`}
|
|
322
|
+
page={page}
|
|
323
|
+
title={title}
|
|
324
|
+
productCard={productCard}
|
|
325
|
+
itemsPerPage={itemsPerPage}
|
|
326
|
+
firstPage={pages[0]}
|
|
327
|
+
shouldShowComparison={showComparisonProducts}
|
|
328
|
+
compareLabel={productComparison?.labels?.compareButton}
|
|
329
|
+
/>
|
|
330
|
+
))}
|
|
331
|
+
</Suspense>
|
|
332
|
+
) : (
|
|
333
|
+
GalleryPageSkeleton
|
|
334
|
+
)}
|
|
380
335
|
{/* Add link to next page. This helps on SEO */}
|
|
381
336
|
{next !== false && (
|
|
382
337
|
<div data-fs-product-listing-pagination="bottom">
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import config from '../discovery.config'
|
|
2
2
|
import pkgJSON from '../package.json'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Next.js instrumentation hook. This is the only place telemetry is booted, so
|
|
6
|
+
* the OTel SDK is registered before any request is served.
|
|
7
|
+
*/
|
|
4
8
|
export async function register() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
// Disabled feature until release date
|
|
10
|
+
const instrumentationEnabled = false
|
|
11
|
+
|
|
12
|
+
// The runtime check must wrap the import instead of guarding with an early
|
|
13
|
+
// return: bundlers replace `process.env.NEXT_RUNTIME` with a literal, so only
|
|
14
|
+
// a positive branch gets dropped from the edge bundle. Left reachable, the
|
|
15
|
+
// OTel SDK pulls in Node builtins (`fs`, `zlib`, `stream`) that fail to resolve.
|
|
16
|
+
if (process.env.NEXT_RUNTIME === 'nodejs' && instrumentationEnabled) {
|
|
9
17
|
const { name, version } = pkgJSON
|
|
18
|
+
|
|
10
19
|
try {
|
|
11
20
|
const { getTelemetryClient } = await import('@faststore/diagnostics')
|
|
12
|
-
console.log('Instrumemtation.ts: Getting telemetry client')
|
|
13
21
|
|
|
14
|
-
return getTelemetryClient({
|
|
22
|
+
return await getTelemetryClient({
|
|
15
23
|
serviceName: config.analytics?.serviceName ?? name,
|
|
16
24
|
version,
|
|
17
25
|
account: config.api.storeId,
|
|
@@ -19,7 +27,7 @@ export async function register() {
|
|
|
19
27
|
packageName: name,
|
|
20
28
|
})
|
|
21
29
|
} catch (error) {
|
|
22
|
-
console.error('Failed to initialize OTEL Instrumentation')
|
|
30
|
+
console.error('Failed to initialize OTEL Instrumentation', error)
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
33
|
}
|
package/src/pages/api/graphql.ts
CHANGED
|
@@ -13,6 +13,9 @@ import { getRequestHostname } from 'src/utils/getRequestHostname'
|
|
|
13
13
|
import { isLocalHost } from 'src/utils/isLocalHost'
|
|
14
14
|
import { shouldForceRefreshTokenForValidateSession } from 'src/utils/validateSessionRefreshToken'
|
|
15
15
|
import { execute } from '../../server'
|
|
16
|
+
import { logger } from '@faststore/diagnostics'
|
|
17
|
+
|
|
18
|
+
const OTELLogger = logger('@faststore/core')
|
|
16
19
|
|
|
17
20
|
const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
|
|
18
21
|
const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour
|
|
@@ -150,6 +153,12 @@ const hasVtexIdclientAutCookie = (request: NextApiRequest): boolean => {
|
|
|
150
153
|
const handler: NextApiHandler = async (request, response) => {
|
|
151
154
|
if (request.method !== 'POST' && request.method !== 'GET') {
|
|
152
155
|
response.status(405).end()
|
|
156
|
+
OTELLogger(
|
|
157
|
+
'info',
|
|
158
|
+
'Invalid request method: %s %s',
|
|
159
|
+
request.method,
|
|
160
|
+
request.url
|
|
161
|
+
)
|
|
153
162
|
|
|
154
163
|
return
|
|
155
164
|
}
|
|
@@ -158,6 +167,15 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
158
167
|
// value is used to cache bust the request if there is a VtexIdclientAutCookie
|
|
159
168
|
const { operation, variables, query, v: value } = parseRequest(request)
|
|
160
169
|
|
|
170
|
+
// Variables and the query body are deliberately not logged: they carry
|
|
171
|
+
// session and customer data.
|
|
172
|
+
OTELLogger(
|
|
173
|
+
'debug',
|
|
174
|
+
'operation: %s hash: %s',
|
|
175
|
+
operation?.__meta__?.operationName,
|
|
176
|
+
operation?.__meta__?.operationHash
|
|
177
|
+
)
|
|
178
|
+
|
|
161
179
|
const isLocal = isLocalHost(getRequestHostname(request.headers.host))
|
|
162
180
|
|
|
163
181
|
if (
|
|
@@ -209,18 +227,21 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
209
227
|
// upstream status) is nested in `originalError`. Recover it so the BFF
|
|
210
228
|
// propagates the real status instead of collapsing everything to 500.
|
|
211
229
|
const fastStoreError = errors.map(recoverFastStoreError).find(Boolean)
|
|
230
|
+
const reportedErrors = errors.map((graphqlError) => {
|
|
231
|
+
const fsError = recoverFastStoreError(graphqlError)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
message: (graphqlError as { message?: string })?.message,
|
|
235
|
+
status: fsError?.extensions.status,
|
|
236
|
+
type: fsError?.extensions.type,
|
|
237
|
+
}
|
|
238
|
+
})
|
|
212
239
|
|
|
213
|
-
console.error(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
message: (graphqlError as { message?: string })?.message,
|
|
220
|
-
status: fsError?.extensions.status,
|
|
221
|
-
type: fsError?.extensions.type,
|
|
222
|
-
}
|
|
223
|
-
})
|
|
240
|
+
console.error('Graphql execution returned with error: ', reportedErrors)
|
|
241
|
+
OTELLogger(
|
|
242
|
+
'error',
|
|
243
|
+
'Graphql execution returned with error: %o',
|
|
244
|
+
reportedErrors
|
|
224
245
|
)
|
|
225
246
|
|
|
226
247
|
const status = fastStoreError?.extensions.status ?? 500
|
|
@@ -304,6 +325,11 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
304
325
|
'Something unexpected occurred querying Graphql endpoint: \n',
|
|
305
326
|
err
|
|
306
327
|
)
|
|
328
|
+
OTELLogger(
|
|
329
|
+
'error',
|
|
330
|
+
'Something unexpected occurred querying Graphql endpoint: %o',
|
|
331
|
+
err
|
|
332
|
+
)
|
|
307
333
|
|
|
308
334
|
if (err instanceof BadRequestError) {
|
|
309
335
|
response.status(400).end()
|