@salesforce/storefront-next-dev 1.1.0-alpha.0 → 1.1.0-alpha.1

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/dist/server.js CHANGED
@@ -12,12 +12,12 @@ import compression from "compression";
12
12
  import zlib from "node:zlib";
13
13
  import morgan from "morgan";
14
14
  import { minimatch } from "minimatch";
15
- import { ROOT_CONTEXT, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
15
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
16
+ import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp, parseTraceParent } from "@opentelemetry/core";
17
+ import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
16
18
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
17
19
  import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
18
- import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp } from "@opentelemetry/core";
19
20
  import { Resource } from "@opentelemetry/resources";
20
- import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
21
21
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
22
22
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
23
23
 
@@ -403,6 +403,43 @@ var MrtConsoleSpanExporter = class extends ConsoleSpanExporter {
403
403
 
404
404
  //#endregion
405
405
  //#region src/otel/setup.ts
406
+ /**
407
+ * A privately-held W3C Trace Context propagator, used to inject `traceparent` onto
408
+ * outbound fetches WITHOUT going through OpenTelemetry's global propagator registry.
409
+ *
410
+ * ## Why not the global propagator?
411
+ *
412
+ * `UndiciInstrumentation` injects outbound headers by calling the *global* registered
413
+ * propagator (`propagation.inject()`). Registering one with `setGlobalPropagator()`
414
+ * works locally, but is a silent no-op on Managed Runtime (MRT):
415
+ *
416
+ * - The MRT Lambda wrapper bundles its own copy of `@opentelemetry/api` (declared
417
+ * `^1.9.1`, with core/sdk at 2.x) and boots first, creating the version-keyed
418
+ * global registry object (`globalThis[Symbol.for('opentelemetry.js.api.1')]`,
419
+ * stamped `version: "1.9.1"`).
420
+ * - Our SDK bundle ships a *different* copy of `@opentelemetry/api` (`1.9.0`). When
421
+ * our code calls `setGlobalPropagator()`, OTel's `registerGlobal()` compares the
422
+ * registry's recorded version against ours with an EXACT string match — `"1.9.1"
423
+ * !== "1.9.0"` — refuses the registration, and reports the error only to OTel's
424
+ * internal diag channel (which we do not wire up). The propagation slot stays
425
+ * empty, so undici's `propagation.inject()` injects nothing and no `traceparent`
426
+ * reaches SCAPI. Proven on soak: `propagation.fields() === []`, 0/198 outbound
427
+ * requests carried a header.
428
+ *
429
+ * This is the same multi-instance split that makes inbound `propagation.extract()`
430
+ * fail (see `express/middleware.ts`, which bypasses it with `parseTraceParent` +
431
+ * `trace.setSpanContext`). MRT's own data-plane tracer solves it the same way: it
432
+ * holds a private `new W3CTraceContextPropagator()` and calls `inject()`/`extract()`
433
+ * on it directly, never touching the global registry.
434
+ *
435
+ * Holding our own instance and injecting it ourselves (in the undici `requestHook`)
436
+ * makes outbound propagation deterministic and independent of MRT's bundle, its OTel
437
+ * version, and its boot order. We deliberately do NOT call `setGlobalPropagator()` —
438
+ * leaving the global propagator as the default no-op guarantees undici's own
439
+ * `propagation.inject()` (which runs right after our hook) adds nothing, so the
440
+ * `traceparent` is written exactly once in every environment.
441
+ */
442
+ const outboundPropagator = new W3CTraceContextPropagator();
406
443
  const SERVICE_NAME = "storefront-next";
407
444
  /**
408
445
  * Initializes OpenTelemetry and returns a Tracer from the provider directly.
@@ -430,17 +467,20 @@ function initTelemetry() {
430
467
  sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() })
431
468
  });
432
469
  provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
433
- provider.register();
434
- propagation.setGlobalPropagator(new W3CTraceContextPropagator());
470
+ provider.register({ propagator: null });
435
471
  if (!globalThis[UNDICI_REGISTERED_KEY]) {
436
472
  globalThis[UNDICI_REGISTERED_KEY] = true;
437
473
  registerInstrumentations({
438
474
  tracerProvider: provider,
439
475
  instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
440
476
  try {
441
- const method = request.method.toUpperCase();
442
- const url = `${request.origin}${request.path}`;
443
- span.updateName(`${method} ${url}`);
477
+ span.updateName("sfnext.fetch");
478
+ } catch {}
479
+ try {
480
+ const ctx = trace.setSpan(context.active(), span);
481
+ outboundPropagator.inject(ctx, request, { set(req, key, value) {
482
+ req.addHeader?.(key, value);
483
+ } });
444
484
  } catch {}
445
485
  } })]
446
486
  });
@@ -463,44 +503,50 @@ function createOtelExpressMiddleware() {
463
503
  try {
464
504
  const url = new URL(req.originalUrl || req.url, "http://localhost").pathname;
465
505
  const method = req.method;
466
- const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);
467
- tracer.startActiveSpan(`[sfnext] server ${method} ${url}`, { attributes: {
468
- "http.request.method": method,
469
- "url.path": url
470
- } }, parentContext, (serverSpan) => {
506
+ const traceparent = req.headers.traceparent;
507
+ const inboundSpanContext = typeof traceparent === "string" ? parseTraceParent(traceparent) : null;
508
+ const parentContext = inboundSpanContext ? trace.setSpanContext(ROOT_CONTEXT, {
509
+ ...inboundSpanContext,
510
+ isRemote: true
511
+ }) : ROOT_CONTEXT;
512
+ tracer.startActiveSpan("sfnext.request", {
513
+ kind: SpanKind.SERVER,
514
+ attributes: {
515
+ [ATTR_HTTP_REQUEST_METHOD]: method,
516
+ [ATTR_URL_PATH]: url
517
+ }
518
+ }, parentContext, (serverSpan) => {
471
519
  try {
472
520
  const spanContext = trace.getSpan(context.active())?.spanContext();
473
521
  if (spanContext) {
474
522
  const flags = spanContext.traceFlags.toString(16).padStart(2, "0");
475
- const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
476
- res.setHeader("traceparent", traceparent);
523
+ const responseTraceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
524
+ res.setHeader("traceparent", responseTraceparent);
477
525
  }
478
526
  } catch {}
479
527
  const serverCtx = context.active();
480
528
  const startTime = performance.now();
481
529
  let streamingSpan = null;
482
- let ttfbMs = 0;
530
+ let firstByteMs = 0;
483
531
  let ended = false;
484
- function recordTTFB() {
532
+ function startStreamingSpan() {
485
533
  if (streamingSpan) return;
486
534
  try {
487
- ttfbMs = Math.round(performance.now() - startTime);
488
- serverSpan.setAttribute("sfnext.ttfb_ms", ttfbMs);
489
- streamingSpan = tracer.startSpan(`[sfnext] response streaming ${method} ${url}`, { attributes: {
490
- "http.request.method": method,
491
- "url.path": url,
492
- "sfnext.ttfb_ms": ttfbMs
535
+ firstByteMs = Math.round(performance.now() - startTime);
536
+ streamingSpan = tracer.startSpan("sfnext.response_streaming", { attributes: {
537
+ [ATTR_HTTP_REQUEST_METHOD]: method,
538
+ [ATTR_URL_PATH]: url
493
539
  } }, serverCtx);
494
540
  } catch {}
495
541
  }
496
542
  const origWriteHead = res.writeHead.bind(res);
497
543
  res.writeHead = ((...args) => {
498
- recordTTFB();
544
+ startStreamingSpan();
499
545
  return origWriteHead(...args);
500
546
  });
501
547
  const origWrite = res.write.bind(res);
502
548
  res.write = ((...args) => {
503
- recordTTFB();
549
+ startStreamingSpan();
504
550
  return origWrite(...args);
505
551
  });
506
552
  function endSpans() {
@@ -510,12 +556,12 @@ function createOtelExpressMiddleware() {
510
556
  const totalMs = Math.round(performance.now() - startTime);
511
557
  const statusCode = res.statusCode;
512
558
  if (streamingSpan) {
513
- streamingSpan.setAttribute("http.streaming_duration_ms", totalMs - ttfbMs);
514
- streamingSpan.setAttribute("http.response.status_code", statusCode);
559
+ streamingSpan.setAttribute("http.streaming_duration_ms", totalMs - firstByteMs);
560
+ streamingSpan.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);
515
561
  if (statusCode >= 500) streamingSpan.setStatus({ code: SpanStatusCode.ERROR });
516
562
  streamingSpan.end();
517
563
  }
518
- serverSpan.setAttribute("http.response.status_code", statusCode);
564
+ serverSpan.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);
519
565
  serverSpan.setAttribute("http.total_duration_ms", totalMs);
520
566
  if (statusCode >= 500) serverSpan.setStatus({ code: SpanStatusCode.ERROR });
521
567
  serverSpan.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/storefront-next-dev",
3
- "version": "1.1.0-alpha.0",
3
+ "version": "1.1.0-alpha.1",
4
4
  "description": "Dev and build tools for SFCC Storefront Next",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -118,7 +118,8 @@
118
118
  },
119
119
  "devDependencies": {
120
120
  "@oclif/test": "^4.1.15",
121
- "@react-router/dev": "7.12.0",
121
+ "@react-router/dev": "7.18.0",
122
+ "@react-router/node": "7.18.0",
122
123
  "@serverless/event-mocks": "1.1.1",
123
124
  "@types/archiver": "^6.0.2",
124
125
  "@types/aws-lambda": "8.10.159",
@@ -139,19 +140,19 @@
139
140
  "@types/react": "19.1.13",
140
141
  "@types/react-dom": "19.1.9",
141
142
  "@types/semver": "7.7.1",
142
- "@vitest/coverage-v8": "4.0.18",
143
+ "@vitest/coverage-v8": "4.1.8",
143
144
  "fs-extra": "11.3.2",
144
145
  "jest": "^29.7.0",
145
146
  "memfs": "^4.42.0",
146
147
  "react": "19.2.3",
147
148
  "react-dom": "19.2.3",
148
- "react-router": "7.12.0",
149
+ "react-router": "7.18.0",
149
150
  "shx": "^0.4.0",
150
151
  "ts-jest": "29.4.1",
151
152
  "tsdown": "^0.15.4",
152
153
  "typescript": "^5.6.0",
153
- "vite": "7.1.7",
154
- "vitest": "4.0.18"
154
+ "vite": "7.3.5",
155
+ "vitest": "4.1.8"
155
156
  },
156
157
  "dependencies": {
157
158
  "@babel/generator": "7.28.5",
@@ -172,7 +173,7 @@
172
173
  "@opentelemetry/sdk-trace-base": "^1.30.0",
173
174
  "@opentelemetry/sdk-trace-node": "^1.30.0",
174
175
  "@opentelemetry/semantic-conventions": "^1.28.0",
175
- "@react-router/express": "7.12.0",
176
+ "@react-router/express": "7.18.0",
176
177
  "@salesforce/b2c-tooling-sdk": "^1.15.0",
177
178
  "archiver": "^7.0.1",
178
179
  "babel-dead-code-elimination": "1.0.12",
@@ -191,11 +192,12 @@
191
192
  "negotiator": "1.0.0",
192
193
  "npm-run-path": "6.0.0",
193
194
  "openapi-typescript": "7.10.1",
195
+ "prettier": "3.6.2",
194
196
  "prompts": "2.4.2",
195
197
  "ts-morph": "^24.0.0",
196
198
  "tsdown": "^0.15.4",
197
199
  "tsx": "^4.20.5",
198
- "undici": "^6.21.3",
200
+ "undici": "7.28.0",
199
201
  "yaml": "^2.8.2",
200
202
  "zod": "4.1.13"
201
203
  },