react-on-rails-pro-node-renderer 16.7.0-rc.2 → 17.0.0-rc.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.
@@ -1,8 +1,18 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.startSsrRequestOptions = void 0;
4
7
  exports.setupTracing = setupTracing;
5
8
  exports.trace = trace;
9
+ exports.resetTracing = resetTracing;
10
+ exports.__resetTracingForTest = __resetTracingForTest;
11
+ exports.setupSubSpan = setupSubSpan;
12
+ exports.subSpan = subSpan;
13
+ exports.resetSubSpan = resetSubSpan;
14
+ exports.__resetSubSpanForTest = __resetSubSpanForTest;
15
+ const log_js_1 = __importDefault(require("./log.js"));
6
16
  const errorReporter_js_1 = require("./errorReporter.js");
7
17
  /* eslint-enable @typescript-eslint/no-empty-object-type */
8
18
  let setupRun = false;
@@ -18,17 +28,19 @@ exports.startSsrRequestOptions = startSsrRequestOptions;
18
28
  * @param options.startSsrRequestOptions - Options used to start a new unit of work for an SSR request.
19
29
  * Should be an object with your integration name as the only property.
20
30
  * It will be passed to the executor.
31
+ * @returns true when this call installed the tracing integration.
21
32
  */
22
33
  function setupTracing(options) {
23
34
  if (setupRun) {
24
35
  (0, errorReporter_js_1.message)('setupTracing called more than once. Currently only one tracing integration can be enabled.');
25
- return;
36
+ return false;
26
37
  }
27
38
  executor = options.executor;
28
39
  if (options.startSsrRequestOptions) {
29
40
  mutableStartSsrRequestOptions = options.startSsrRequestOptions;
30
41
  }
31
42
  setupRun = true;
43
+ return true;
32
44
  }
33
45
  /**
34
46
  * Reports a unit of work to the tracing service, if any.
@@ -36,4 +48,106 @@ function setupTracing(options) {
36
48
  function trace(fn, unitOfWorkOptions) {
37
49
  return executor(fn, unitOfWorkOptions);
38
50
  }
51
+ /**
52
+ * Resets the installed tracing executor + startSsrRequestOptions back to
53
+ * defaults. Integrations use this during lifecycle teardown or failed initialization cleanup.
54
+ *
55
+ * Caller contract: only integrations that own the active tracing lifecycle
56
+ * should call this, and only while tearing that lifecycle down.
57
+ */
58
+ function resetTracing() {
59
+ executor = (fn) => fn();
60
+ mutableStartSsrRequestOptions = () => ({});
61
+ setupRun = false;
62
+ }
63
+ /**
64
+ * Test-only: reset the installed tracing executor + startSsrRequestOptions back
65
+ * to defaults. Not part of the public api — do not re-export from
66
+ * `integrations/api.ts`.
67
+ */
68
+ // eslint-disable-next-line no-underscore-dangle
69
+ function __resetTracingForTest() {
70
+ resetTracing();
71
+ }
72
+ const noOpSubSpanController = {
73
+ setAttributes() { },
74
+ };
75
+ const defaultSubSpan = (_opts, fn) => fn(noOpSubSpanController);
76
+ let subSpanImpl = defaultSubSpan;
77
+ let subSpanSetupRun = false;
78
+ /**
79
+ * Install a sub-span implementation. Integrations call this from their `init()`
80
+ * to start receiving sub-span events. If never called, sub-spans are no-ops.
81
+ * @returns true when this call installed the sub-span integration.
82
+ */
83
+ function setupSubSpan(impl) {
84
+ if (subSpanSetupRun) {
85
+ (0, errorReporter_js_1.message)('setupSubSpan called more than once. Only one sub-span integration can be enabled.');
86
+ return false;
87
+ }
88
+ subSpanImpl = impl;
89
+ subSpanSetupRun = true;
90
+ return true;
91
+ }
92
+ /**
93
+ * Wrap an async function in a named sub-span. Safe to call even when no
94
+ * integration is installed — defaults to passing through to `fn`.
95
+ *
96
+ * The wrapped function receives a {@link SubSpanController} it can use to
97
+ * attach attributes that are only known after the work runs (e.g., response
98
+ * byte counts). With no integration installed, attribute updates are dropped.
99
+ *
100
+ * If the installed implementation throws or rejects before invoking `fn`, the
101
+ * caller is shielded: `fn` is still executed outside any sub-span (with the
102
+ * no-op controller) and its result returned. If the implementation fails
103
+ * after invoking `fn`, the error is rethrown so `fn` is never run twice.
104
+ */
105
+ function subSpan(opts, fn) {
106
+ let invoked = false;
107
+ const wrappedFn = (controller) => {
108
+ invoked = true;
109
+ return fn(controller);
110
+ };
111
+ try {
112
+ return Promise.resolve(subSpanImpl(opts, wrappedFn)).catch((err) => {
113
+ if (invoked) {
114
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
115
+ }
116
+ // log.warn (not message) for the silent-fallback path. message() goes to
117
+ // log.error + external notifiers (Sentry/Bugsnag/etc.) on every request,
118
+ // which is too noisy for a per-request recoverable failure where fn() is
119
+ // still executed. log.warn surfaces the broken integration without
120
+ // paging the on-call team for every render.
121
+ log_js_1.default.warn({ err }, 'subSpan implementation rejected before invoking fn(); running fn() without a span');
122
+ return wrappedFn(noOpSubSpanController);
123
+ });
124
+ }
125
+ catch (err) {
126
+ if (invoked) {
127
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
128
+ }
129
+ log_js_1.default.warn({ err }, 'subSpan implementation threw before invoking fn(); running fn() without a span');
130
+ return wrappedFn(noOpSubSpanController);
131
+ }
132
+ }
133
+ /**
134
+ * Resets the installed sub-span implementation back to the default pass-through.
135
+ * Integrations use this during lifecycle teardown or failed initialization cleanup.
136
+ *
137
+ * Caller contract: only integrations that own the active sub-span lifecycle
138
+ * should call this, and only while tearing that lifecycle down.
139
+ */
140
+ function resetSubSpan() {
141
+ subSpanImpl = defaultSubSpan;
142
+ subSpanSetupRun = false;
143
+ }
144
+ /**
145
+ * Test-only: reset the installed sub-span implementation back to the default
146
+ * pass-through. Not part of the public api — do not re-export from
147
+ * `integrations/api.ts`.
148
+ */
149
+ // eslint-disable-next-line no-underscore-dangle
150
+ function __resetSubSpanForTest() {
151
+ resetSubSpan();
152
+ }
39
153
  //# sourceMappingURL=tracing.js.map
@@ -0,0 +1,2 @@
1
+ export declare function resetOpenTelemetryForTest(): Promise<void>;
2
+ //# sourceMappingURL=opentelemetry.d.ts.map
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resetOpenTelemetryForTest = resetOpenTelemetryForTest;
37
+ const opentelemetryState_js_1 = require("../shared/opentelemetryState.js");
38
+ const tracing_js_1 = require("../shared/tracing.js");
39
+ const fastifyConfig = __importStar(require("../worker/fastifyConfig.js"));
40
+ const shutdownHooks = __importStar(require("../worker/shutdownHooks.js"));
41
+ async function resetOpenTelemetryForTest() {
42
+ const tracerProvider = (0, opentelemetryState_js_1.getOpenTelemetryTracerProvider)();
43
+ if (tracerProvider) {
44
+ await tracerProvider.shutdown();
45
+ (0, opentelemetryState_js_1.setOpenTelemetryTracerProvider)(null);
46
+ }
47
+ (0, tracing_js_1.resetSubSpan)();
48
+ (0, tracing_js_1.resetTracing)();
49
+ // eslint-disable-next-line no-underscore-dangle
50
+ fastifyConfig.__resetFastifyConfigFunctionsForTest();
51
+ // eslint-disable-next-line no-underscore-dangle
52
+ shutdownHooks.__resetWorkerShutdownHooksForTest();
53
+ /* eslint-disable @typescript-eslint/no-require-imports, global-require */
54
+ try {
55
+ const otelApi = require('@opentelemetry/api');
56
+ otelApi.trace.disable();
57
+ otelApi.context.disable();
58
+ otelApi.propagation.disable();
59
+ otelApi.diag.disable();
60
+ }
61
+ catch {
62
+ // OTel API not installed - nothing to disable.
63
+ }
64
+ /* eslint-enable @typescript-eslint/no-require-imports, global-require */
65
+ }
66
+ //# sourceMappingURL=opentelemetry.js.map
@@ -10,6 +10,7 @@ exports.checkProtocolVersion = checkProtocolVersion;
10
10
  */
11
11
  const packageJson_js_1 = __importDefault(require("../shared/packageJson.js"));
12
12
  const log_js_1 = __importDefault(require("../shared/log.js"));
13
+ const sensitiveKeys_js_1 = require("../shared/sensitiveKeys.js");
13
14
  const NODE_ENV = process.env.NODE_ENV || 'production';
14
15
  // Cache to store version comparison results to avoid repeated normalization and logging
15
16
  // Key: gemVersion string, Value: boolean (true if matches, false if mismatch)
@@ -43,7 +44,7 @@ function checkProtocolVersion(body) {
43
44
  status: 412,
44
45
  data: `Unsupported renderer protocol version ${reqProtocolVersion
45
46
  ? `request protocol ${reqProtocolVersion}`
46
- : `MISSING with body ${JSON.stringify(body)}`} does not match installed renderer protocol ${packageJson_js_1.default.protocolVersion} for version ${packageJson_js_1.default.version}.
47
+ : `MISSING (received fields: ${(0, sensitiveKeys_js_1.sanitizeBodyKeys)(body).join(', ') || '(none)'})`} does not match installed renderer protocol ${packageJson_js_1.default.protocolVersion} for version ${packageJson_js_1.default.version}.
47
48
  Update either the renderer or the Rails server`,
48
49
  };
49
50
  }
@@ -0,0 +1,19 @@
1
+ import type { FastifyInstance } from './types.js';
2
+ export type FastifyConfigFunction = (app: FastifyInstance) => void;
3
+ /**
4
+ * Configures the Fastify instance before starting the server.
5
+ *
6
+ * This module intentionally has no runtime dependency on `worker.ts` or
7
+ * `fastify`, so integrations can register instrumentation before Fastify is
8
+ * required by the worker module graph.
9
+ */
10
+ export declare function registerFastifyConfigFunction(configFunction: FastifyConfigFunction): () => void;
11
+ /**
12
+ * Public one-way registration API for custom entrypoints and integrations.
13
+ * Internal callers use registerFastifyConfigFunction() when they need the
14
+ * unregister callback during failed initialization or shutdown cleanup.
15
+ */
16
+ export declare function configureFastify(configFunction: FastifyConfigFunction): void;
17
+ export declare function applyFastifyConfigFunctions(app: FastifyInstance): void;
18
+ export declare function __resetFastifyConfigFunctionsForTest(): void;
19
+ //# sourceMappingURL=fastifyConfig.d.ts.map
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerFastifyConfigFunction = registerFastifyConfigFunction;
4
+ exports.configureFastify = configureFastify;
5
+ exports.applyFastifyConfigFunctions = applyFastifyConfigFunctions;
6
+ exports.__resetFastifyConfigFunctionsForTest = __resetFastifyConfigFunctionsForTest;
7
+ const fastifyConfigFunctions = [];
8
+ /**
9
+ * Configures the Fastify instance before starting the server.
10
+ *
11
+ * This module intentionally has no runtime dependency on `worker.ts` or
12
+ * `fastify`, so integrations can register instrumentation before Fastify is
13
+ * required by the worker module graph.
14
+ */
15
+ function registerFastifyConfigFunction(configFunction) {
16
+ fastifyConfigFunctions.push(configFunction);
17
+ return () => {
18
+ const index = fastifyConfigFunctions.indexOf(configFunction);
19
+ if (index >= 0) {
20
+ fastifyConfigFunctions.splice(index, 1);
21
+ }
22
+ };
23
+ }
24
+ /**
25
+ * Public one-way registration API for custom entrypoints and integrations.
26
+ * Internal callers use registerFastifyConfigFunction() when they need the
27
+ * unregister callback during failed initialization or shutdown cleanup.
28
+ */
29
+ function configureFastify(configFunction) {
30
+ registerFastifyConfigFunction(configFunction);
31
+ }
32
+ function applyFastifyConfigFunctions(app) {
33
+ fastifyConfigFunctions.forEach((configFunction) => {
34
+ configFunction(app);
35
+ });
36
+ }
37
+ // eslint-disable-next-line no-underscore-dangle
38
+ function __resetFastifyConfigFunctionsForTest() {
39
+ fastifyConfigFunctions.length = 0;
40
+ }
41
+ //# sourceMappingURL=fastifyConfig.js.map
@@ -6,6 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const cluster_1 = __importDefault(require("cluster"));
7
7
  const utils_js_1 = require("../shared/utils.js");
8
8
  const log_js_1 = __importDefault(require("../shared/log.js"));
9
+ const shutdownHooks_js_1 = require("./shutdownHooks.js");
10
+ function errorCode(error) {
11
+ const code = error?.code;
12
+ return typeof code === 'string' ? code : undefined;
13
+ }
9
14
  const handleGracefulShutdown = (app) => {
10
15
  const { worker } = cluster_1.default;
11
16
  if (!worker) {
@@ -14,12 +19,54 @@ const handleGracefulShutdown = (app) => {
14
19
  }
15
20
  let activeRequestsCount = 0;
16
21
  let isShuttingDown = false;
22
+ let isDestroying = false;
23
+ const destroyWorkerAfterShutdownHooks = (context) => {
24
+ if (isDestroying) {
25
+ return;
26
+ }
27
+ isDestroying = true;
28
+ log_js_1.default.debug('Worker #%d running shutdown hooks before shutdown after %s', worker.id, context);
29
+ let workerDestroyed = false;
30
+ const destroyWorker = () => {
31
+ if (workerDestroyed) {
32
+ return;
33
+ }
34
+ workerDestroyed = true;
35
+ worker.destroy();
36
+ };
37
+ const shutdownTimeout = setTimeout(() => {
38
+ log_js_1.default.warn('Worker #%d: shutdown hooks timed out, forcing worker.destroy()', worker.id);
39
+ destroyWorker();
40
+ }, shutdownHooks_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS);
41
+ const shutdownHooksPromise = (0, shutdownHooks_js_1.runWorkerShutdownHooks)();
42
+ void shutdownHooksPromise
43
+ .catch((error) => {
44
+ log_js_1.default.warn({ msg: 'Error running worker shutdown hooks before worker shutdown', error });
45
+ })
46
+ .finally(() => {
47
+ clearTimeout(shutdownTimeout);
48
+ destroyWorker();
49
+ });
50
+ };
51
+ const disconnectWorker = () => {
52
+ try {
53
+ worker.disconnect();
54
+ }
55
+ catch (error) {
56
+ if (errorCode(error) === 'ERR_IPC_DISCONNECTED') {
57
+ log_js_1.default.debug('Worker #%d IPC channel was already disconnected during graceful shutdown', worker.id);
58
+ }
59
+ else {
60
+ log_js_1.default.warn({ msg: 'Error disconnecting worker during graceful shutdown', error });
61
+ }
62
+ }
63
+ };
17
64
  // Helper to decrement counter and potentially kill worker
18
65
  const decrementAndMaybeShutdown = (context) => {
19
66
  activeRequestsCount -= 1;
20
67
  if (isShuttingDown && activeRequestsCount === 0) {
21
68
  log_js_1.default.debug('Worker #%d has no active requests after %s, killing the worker', worker.id, context);
22
- worker.destroy();
69
+ destroyWorkerAfterShutdownHooks(context);
23
70
  }
24
71
  };
25
72
  process.on('message', (msg) => {
@@ -28,11 +75,11 @@ const handleGracefulShutdown = (app) => {
28
75
  isShuttingDown = true;
29
76
  if (activeRequestsCount === 0) {
30
77
  log_js_1.default.debug('Worker #%d has no active requests, killing the worker', worker.id);
31
- worker.destroy();
78
+ destroyWorkerAfterShutdownHooks('shutdown message');
32
79
  }
33
80
  else {
34
81
  log_js_1.default.debug('Worker #%d has "%d" active requests, disconnecting the worker', worker.id, activeRequestsCount);
35
- worker.disconnect();
82
+ disconnectWorker();
36
83
  }
37
84
  }
38
85
  });
@@ -7,6 +7,13 @@ exports.handleIncrementalRenderRequest = handleIncrementalRenderRequest;
7
7
  const handleRenderRequest_1 = require("./handleRenderRequest");
8
8
  const log_1 = __importDefault(require("../shared/log"));
9
9
  const utils_1 = require("../shared/utils");
10
+ const tracing_js_1 = require("../shared/tracing.js");
11
+ class InvalidIncrementalRenderChunkError extends Error {
12
+ constructor() {
13
+ super('Invalid incremental render chunk received, missing properties');
14
+ this.name = 'InvalidIncrementalRenderChunkError';
15
+ }
16
+ }
10
17
  function assertIsUpdateChunk(value) {
11
18
  if (typeof value !== 'object' ||
12
19
  value === null ||
@@ -14,7 +21,7 @@ function assertIsUpdateChunk(value) {
14
21
  !('updateChunk' in value) ||
15
22
  (typeof value.bundleTimestamp !== 'string' && typeof value.bundleTimestamp !== 'number') ||
16
23
  typeof value.updateChunk !== 'string') {
17
- throw new Error('Invalid incremental render chunk received, missing properties');
24
+ throw new InvalidIncrementalRenderChunkError();
18
25
  }
19
26
  }
20
27
  function assertFirstIncrementalRenderRequestChunk(chunk) {
@@ -63,6 +70,8 @@ async function handleIncrementalRenderRequest(initial) {
63
70
  const { renderingRequest, onRequestClosedUpdateChunk } = firstRequestChunk;
64
71
  try {
65
72
  // Call handleRenderRequest internally to handle all validation and VM execution
73
+ // handleRenderRequest is called directly without a TracingContext from worker.ts's
74
+ // trace() wrapper, so there is no tracingContext to forward for its error path.
66
75
  const { response, executionContext } = await (0, handleRenderRequest_1.handleRenderRequest)({
67
76
  renderingRequest,
68
77
  bundleTimestamp,
@@ -82,13 +91,21 @@ async function handleIncrementalRenderRequest(initial) {
82
91
  add: async (chunk) => {
83
92
  try {
84
93
  assertIsUpdateChunk(chunk);
85
- const bundlePath = (0, utils_1.getRequestBundleFilePath)(chunk.bundleTimestamp);
86
- await executionContext.runInVM(chunk.updateChunk, bundlePath).catch((err) => {
87
- log_1.default.error({ msg: 'Error running incremental render chunk', err, chunk });
94
+ await (0, tracing_js_1.subSpan)({ name: 'ror.incremental.process_chunk' }, async () => {
95
+ const bundlePath = (0, utils_1.getRequestBundleFilePath)(chunk.bundleTimestamp);
96
+ const result = await executionContext.runInVM(chunk.updateChunk, bundlePath);
97
+ if ((0, utils_1.isErrorRenderResult)(result)) {
98
+ throw new Error(result.exceptionMessage);
99
+ }
88
100
  });
89
101
  }
90
102
  catch (err) {
91
- log_1.default.error({ msg: 'Invalid incremental render chunk', err, chunk });
103
+ if (err instanceof InvalidIncrementalRenderChunkError) {
104
+ log_1.default.error({ msg: 'Invalid incremental render chunk', err, chunk });
105
+ }
106
+ else {
107
+ log_1.default.error({ msg: 'Error running incremental render chunk', err, chunk });
108
+ }
92
109
  }
93
110
  },
94
111
  handleRequestClosed: () => {