@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.5.1",
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/lighthouse": "4.5.1",
75
- "@faststore/ui": "4.5.1",
76
- "@faststore/diagnostics": "4.5.1",
77
- "@faststore/sdk": "4.5.1",
78
- "@faststore/api": "4.5.1"
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.7",
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.7"
102
+ "vitest": "4.0.18"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "graphql": "16.11.0",
@@ -94,7 +94,6 @@ function ProductGrid({
94
94
  <ProductGridSkeleton
95
95
  aspectRatio={aspectRatio}
96
96
  loading={products.length === 0}
97
- count={pageSize}
98
97
  >
99
98
  <UIProductGrid
100
99
  data-af-element={searchId && 'search-result'}
@@ -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: count }, (_, index) => (
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 (redirectUrl) {
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, useEffect, useState, type MouseEvent } from 'react'
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
- Each page has its own Suspense boundary so a refetch on page N
352
- does not unmount pages that already have data (avoids CLS when
353
- returning from a PDP with infinite-scroll pages restored).
354
- GalleryPageHeightLock keeps the last measured page height while
355
- skeletons are shown so the list does not collapse — including
356
- the initial !hasProductsLoaded frame. */}
357
- {pages.map((page) => (
358
- <GalleryPageHeightLock key={`gallery-page-${page}`} page={page}>
359
- {hasProductsLoaded ? (
360
- <Suspense
361
- fallback={
362
- <ProductGridSkeleton loading count={itemsPerPage} />
363
- }
364
- >
365
- <ProductGalleryPage
366
- page={page}
367
- title={title}
368
- productCard={productCard}
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">
@@ -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
- if (
6
- process.env.NEXT_RUNTIME === 'nodejs' &&
7
- config.analytics.otelEnabled === true
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
  }
@@ -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
- 'Graphql execution returned with error: ',
215
- errors.map((graphqlError) => {
216
- const fsError = recoverFastStoreError(graphqlError)
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()