@zauru-sdk/utils 6.28.0 → 6.29.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.
@@ -51,12 +51,65 @@ let clientKey = null;
51
51
  const sequenceTokens = {};
52
52
  /** Serializes PutLogEvents so sequence tokens are not overwritten. */
53
53
  let putChain = Promise.resolve();
54
+ let processHandlersInstalled = false;
55
+ let loggedConfigOnce = false;
54
56
  function envTrim(name) {
55
57
  const v = process.env[name];
56
58
  if (!v || v.trim() === "")
57
59
  return undefined;
58
60
  return v.trim();
59
61
  }
62
+ /**
63
+ * Netlify streaming functions freeze the isolate when the response ends.
64
+ * `waitUntil` keeps the Lambda awake until PutLogEvents finishes.
65
+ */
66
+ function keepIsolateAlive(work) {
67
+ const g = globalThis;
68
+ try {
69
+ g.waitUntil?.(work);
70
+ g.Netlify?.waitUntil?.(work);
71
+ }
72
+ catch {
73
+ // Hosts without waitUntil still await in the request path.
74
+ }
75
+ }
76
+ function logConfigOnce() {
77
+ if (loggedConfigOnce)
78
+ return;
79
+ loggedConfigOnce = true;
80
+ const cfg = resolveCloudWatchConfig();
81
+ if (cfg.enabled) {
82
+ console.log(`[observability] CloudWatch enabled group=${cfg.logGroup} env=${cfg.appEnv} creds=${cfg.prefixLabel} region=${cfg.region}`);
83
+ return;
84
+ }
85
+ console.log(`[observability] CloudWatch disabled: ${cfg.skipReason}`);
86
+ }
87
+ /**
88
+ * Catch throws that happen after Remix already returned 200 (fire-and-forget
89
+ * async work). Remix `handleError` does not see those.
90
+ */
91
+ export function installProcessErrorHandlers() {
92
+ if (processHandlersInstalled)
93
+ return;
94
+ if (typeof process === "undefined" || typeof process.on !== "function") {
95
+ return;
96
+ }
97
+ processHandlersInstalled = true;
98
+ const report = (source, reason) => {
99
+ const err = reason instanceof Error ? reason : new Error(String(reason));
100
+ void logError("server", {
101
+ message: err.message,
102
+ stack: err.stack,
103
+ source,
104
+ });
105
+ };
106
+ process.on("unhandledRejection", (reason) => {
107
+ report("unhandledRejection", reason);
108
+ });
109
+ process.on("uncaughtException", (err) => {
110
+ report("uncaughtException", err);
111
+ });
112
+ }
60
113
  function resolveAwsCreds() {
61
114
  for (const c of AWS_CANDIDATES) {
62
115
  const region = envTrim(c.region);
@@ -201,9 +254,9 @@ async function fetchSequenceToken(cw, logGroup, stream) {
201
254
  const res = await cw.send(new DescribeLogStreamsCommand({
202
255
  logGroupName: logGroup,
203
256
  logStreamNamePrefix: stream,
204
- limit: 1,
205
257
  }));
206
- return res.logStreams?.[0]?.uploadSequenceToken;
258
+ const match = res.logStreams?.find((s) => s.logStreamName === stream);
259
+ return match?.uploadSequenceToken;
207
260
  }
208
261
  catch {
209
262
  return undefined;
@@ -350,6 +403,7 @@ async function emitToCloudWatch(origin, lines) {
350
403
  * For client/server also prints with `console.error` (Netlify fallback).
351
404
  */
352
405
  export async function logEvent(origin, payload) {
406
+ logConfigOnce();
353
407
  const line = buildLogLine(origin, {
354
408
  ...payload,
355
409
  appVersion: payload.appVersion ?? getDefaultAppVersion(),
@@ -360,7 +414,9 @@ export async function logEvent(origin, payload) {
360
414
  else {
361
415
  console.log(`[observability] ${line}`);
362
416
  }
363
- return emitToCloudWatch(origin, [line]);
417
+ const pending = emitToCloudWatch(origin, [line]);
418
+ keepIsolateAlive(pending);
419
+ return pending;
364
420
  }
365
421
  /**
366
422
  * Sends an error event to CloudWatch (and console.error as fallback).
@@ -4,5 +4,7 @@
4
4
  * Import from `@zauru-sdk/utils/cloudwatch.server` (not the main barrel) so
5
5
  * the AWS SDK stays out of the client bundle.
6
6
  */
7
- export { isCloudWatchEnabled, logError, logEvent, resolveCloudWatchConfig, withBackgroundLogs, } from "./cloudwatch-logger.server.js";
7
+ import { installProcessErrorHandlers } from "./cloudwatch-logger.server.js";
8
+ installProcessErrorHandlers();
9
+ export { installProcessErrorHandlers, isCloudWatchEnabled, logError, logEvent, resolveCloudWatchConfig, withBackgroundLogs, } from "./cloudwatch-logger.server.js";
8
10
  export { createClientErrorsAction, handleError, } from "./cloudwatch-remix.server.js";
@@ -5,4 +5,6 @@
5
5
  * export { handleRequest as default, handleError } from "@zauru-sdk/utils/remix-entry.server";
6
6
  * ```
7
7
  */
8
+ import { installProcessErrorHandlers } from "./cloudwatch-logger.server.js";
9
+ installProcessErrorHandlers();
8
10
  export { handleRequest, handleError } from "./remix-handle-request.server.js";
@@ -3,6 +3,7 @@ import { PassThrough } from "node:stream";
3
3
  import { createReadableStreamFromReadable } from "@remix-run/node";
4
4
  import { RemixServer } from "@remix-run/react";
5
5
  import { renderToPipeableStream } from "react-dom/server";
6
+ import { logError } from "./cloudwatch-logger.server.js";
6
7
  import { handleError } from "./cloudwatch-remix.server.js";
7
8
  const ABORT_DELAY = 5_000;
8
9
  const RemixServerComponent = RemixServer;
@@ -53,6 +54,14 @@ function handleBotRequest(request, responseStatusCode, responseHeaders, remixCon
53
54
  responseStatusCode = 500;
54
55
  if (shellRendered) {
55
56
  console.error(error);
57
+ const err = error instanceof Error ? error : new Error(String(error));
58
+ void logError("server", {
59
+ message: err.message,
60
+ stack: err.stack,
61
+ url: request.url,
62
+ method: request.method,
63
+ source: "ssr",
64
+ });
56
65
  }
57
66
  },
58
67
  });
@@ -85,6 +94,14 @@ function handleBrowserRequest(request, responseStatusCode, responseHeaders, remi
85
94
  responseStatusCode = 500;
86
95
  if (shellRendered) {
87
96
  console.error(error);
97
+ const err = error instanceof Error ? error : new Error(String(error));
98
+ void logError("server", {
99
+ message: err.message,
100
+ stack: err.stack,
101
+ url: request.url,
102
+ method: request.method,
103
+ source: "ssr",
104
+ });
88
105
  }
89
106
  },
90
107
  });
@@ -1,5 +1,10 @@
1
1
  export type LogOrigin = "client" | "server" | "background";
2
2
  export type LogLevel = "error" | "info" | "warn";
3
+ /**
4
+ * Catch throws that happen after Remix already returned 200 (fire-and-forget
5
+ * async work). Remix `handleError` does not see those.
6
+ */
7
+ export declare function installProcessErrorHandlers(): void;
3
8
  export type CloudWatchConfigInfo = {
4
9
  enabled: boolean;
5
10
  logGroup?: string;
@@ -1,8 +1,2 @@
1
- /**
2
- * Server-only CloudWatch observability for Remix webapps.
3
- *
4
- * Import from `@zauru-sdk/utils/cloudwatch.server` (not the main barrel) so
5
- * the AWS SDK stays out of the client bundle.
6
- */
7
- export { isCloudWatchEnabled, logError, logEvent, resolveCloudWatchConfig, withBackgroundLogs, type CloudWatchConfigInfo, type LogErrorResult, type LogLevel, type LogOrigin, type LogPayload, } from "./cloudwatch-logger.server.js";
1
+ export { installProcessErrorHandlers, isCloudWatchEnabled, logError, logEvent, resolveCloudWatchConfig, withBackgroundLogs, type CloudWatchConfigInfo, type LogErrorResult, type LogLevel, type LogOrigin, type LogPayload, } from "./cloudwatch-logger.server.js";
8
2
  export { createClientErrorsAction, handleError, } from "./cloudwatch-remix.server.js";
@@ -1,8 +1 @@
1
- /**
2
- * Remix 2 SSR entry + CloudWatch `handleError`.
3
- *
4
- * ```ts
5
- * export { handleRequest as default, handleError } from "@zauru-sdk/utils/remix-entry.server";
6
- * ```
7
- */
8
1
  export { handleRequest, handleError } from "./remix-handle-request.server.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/utils",
3
- "version": "6.28.0",
3
+ "version": "6.29.0",
4
4
  "description": "Utilidades, parseo de request y demás para desarrollo de webapps de Zauru",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -78,5 +78,5 @@
78
78
  "@zauru-sdk/types": "^6.29.0",
79
79
  "moment": "^2.30.1"
80
80
  },
81
- "gitHead": "671859beecfce09a1ccf138055a4de26a8c1f059"
81
+ "gitHead": "4355159e008ace641976ba4c5598fc3c7a8e8263"
82
82
  }