@salesforce/storefront-next-dev 1.0.1 → 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 { SpanStatusCode, context, 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
- import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
18
- import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
19
+ import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
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.
@@ -425,18 +462,25 @@ const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
425
462
  function initTelemetry() {
426
463
  if (cachedTracer) return cachedTracer;
427
464
  try {
428
- const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
465
+ const provider = new NodeTracerProvider({
466
+ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),
467
+ sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() })
468
+ });
429
469
  provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
430
- provider.register();
470
+ provider.register({ propagator: null });
431
471
  if (!globalThis[UNDICI_REGISTERED_KEY]) {
432
472
  globalThis[UNDICI_REGISTERED_KEY] = true;
433
473
  registerInstrumentations({
434
474
  tracerProvider: provider,
435
475
  instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
436
476
  try {
437
- const method = request.method.toUpperCase();
438
- const url = `${request.origin}${request.path}`;
439
- 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
+ } });
440
484
  } catch {}
441
485
  } })]
442
486
  });
@@ -459,43 +503,50 @@ function createOtelExpressMiddleware() {
459
503
  try {
460
504
  const url = new URL(req.originalUrl || req.url, "http://localhost").pathname;
461
505
  const method = req.method;
462
- tracer.startActiveSpan(`[sfnext] server ${method} ${url}`, { attributes: {
463
- "http.request.method": method,
464
- "url.path": url
465
- } }, (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) => {
466
519
  try {
467
520
  const spanContext = trace.getSpan(context.active())?.spanContext();
468
521
  if (spanContext) {
469
522
  const flags = spanContext.traceFlags.toString(16).padStart(2, "0");
470
- const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
471
- res.setHeader("traceparent", traceparent);
523
+ const responseTraceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
524
+ res.setHeader("traceparent", responseTraceparent);
472
525
  }
473
526
  } catch {}
474
527
  const serverCtx = context.active();
475
528
  const startTime = performance.now();
476
529
  let streamingSpan = null;
477
- let ttfbMs = 0;
530
+ let firstByteMs = 0;
478
531
  let ended = false;
479
- function recordTTFB() {
532
+ function startStreamingSpan() {
480
533
  if (streamingSpan) return;
481
534
  try {
482
- ttfbMs = Math.round(performance.now() - startTime);
483
- serverSpan.setAttribute("sfnext.ttfb_ms", ttfbMs);
484
- streamingSpan = tracer.startSpan(`[sfnext] response streaming ${method} ${url}`, { attributes: {
485
- "http.request.method": method,
486
- "url.path": url,
487
- "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
488
539
  } }, serverCtx);
489
540
  } catch {}
490
541
  }
491
542
  const origWriteHead = res.writeHead.bind(res);
492
543
  res.writeHead = ((...args) => {
493
- recordTTFB();
544
+ startStreamingSpan();
494
545
  return origWriteHead(...args);
495
546
  });
496
547
  const origWrite = res.write.bind(res);
497
548
  res.write = ((...args) => {
498
- recordTTFB();
549
+ startStreamingSpan();
499
550
  return origWrite(...args);
500
551
  });
501
552
  function endSpans() {
@@ -505,12 +556,12 @@ function createOtelExpressMiddleware() {
505
556
  const totalMs = Math.round(performance.now() - startTime);
506
557
  const statusCode = res.statusCode;
507
558
  if (streamingSpan) {
508
- streamingSpan.setAttribute("http.streaming_duration_ms", totalMs - ttfbMs);
509
- 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);
510
561
  if (statusCode >= 500) streamingSpan.setStatus({ code: SpanStatusCode.ERROR });
511
562
  streamingSpan.end();
512
563
  }
513
- serverSpan.setAttribute("http.response.status_code", statusCode);
564
+ serverSpan.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);
514
565
  serverSpan.setAttribute("http.total_duration_ms", totalMs);
515
566
  if (statusCode >= 500) serverSpan.setStatus({ code: SpanStatusCode.ERROR });
516
567
  serverSpan.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/storefront-next-dev",
3
- "version": "1.0.1",
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",
@@ -110,15 +110,16 @@
110
110
  "license": "Apache-2.0",
111
111
  "peerDependencies": {
112
112
  "@react-router/dev": ">=7.0.0",
113
+ "@salesforce/storefront-next-runtime": "^1.1.0-alpha.0",
113
114
  "react": ">=19.0.0",
114
115
  "react-dom": ">=19.0.0",
115
116
  "react-router": ">=7.0.0",
116
- "vite": ">=7.0.0",
117
- "@salesforce/storefront-next-runtime": "1.0.1"
117
+ "vite": ">=7.0.0"
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,8 +173,8 @@
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
- "@salesforce/b2c-tooling-sdk": "^1.11.0",
176
+ "@react-router/express": "7.18.0",
177
+ "@salesforce/b2c-tooling-sdk": "^1.15.0",
177
178
  "archiver": "^7.0.1",
178
179
  "babel-dead-code-elimination": "1.0.12",
179
180
  "chalk": "^5.3.0",
@@ -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
  },