| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | type DesktopUpdateCheckResult, | 7 | type DesktopUpdateCheckResult, |
| 8 | type DesktopUpdateState, | 8 | type DesktopUpdateState, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| | 10 | import { errorTag } from "@t3tools/shared/observability"; |
| 10 | import * as Cause from "effect/Cause"; | 11 | import * as Cause from "effect/Cause"; |
| 11 | import * as Context from "effect/Context"; | 12 | import * as Context from "effect/Context"; |
| 12 | import * as DateTime from "effect/DateTime"; | 13 | import * as DateTime from "effect/DateTime"; |
| ... | @@ -196,6 +197,63 @@ const { | ... | @@ -196,6 +197,63 @@ const { |
| 196 | logError: logUpdaterError, | 197 | logError: logUpdaterError, |
| 197 | } = DesktopObservability.makeComponentLogger("desktop-updater"); | 198 | } = DesktopObservability.makeComponentLogger("desktop-updater"); |
| 198 | | 199 | |
| | 200 | function redactMessage(message: string): string { |
| | 201 | // Updater HTTP errors append response bodies and headers after the first line. |
| | 202 | return (message.split(/[\r\n]/, 1)[0] ?? "") |
| | 203 | .replace(/\b(?:https?|file):\/\/[^\s"'<>]+/gi, "[redacted URL]") |
| | 204 | .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/gi, "[redacted credential]") |
| | 205 | .replace( |
| | 206 | /\b(?:authorization|proxy-authorization|cookie|set-cookie)\s*[:=].*$/gi, |
| | 207 | "[redacted header]", |
| | 208 | ) |
| | 209 | .replace( |
| | 210 | /\b[\w-]*(?:token|password|secret|credential|api[_-]?key)[\w-]*\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, |
| | 211 | "[redacted credential]", |
| | 212 | ) |
| | 213 | .slice(0, 512); |
| | 214 | } |
| | 215 | |
| | 216 | function summarizeCause(cause: unknown, depth: number): string { |
| | 217 | if (depth >= 3) return "[truncated cause]"; |
| | 218 | if (Cause.isCause(cause)) { |
| | 219 | return cause.reasons |
| | 220 | .slice(0, 3) |
| | 221 | .map((reason) => |
| | 222 | reason._tag === "Fail" |
| | 223 | ? summarizeCause(reason.error, depth + 1) |
| | 224 | : reason._tag === "Die" |
| | 225 | ? summarizeCause(reason.defect, depth + 1) |
| | 226 | : "Interrupted", |
| | 227 | ) |
| | 228 | .join("; "); |
| | 229 | } |
| | 230 | if (typeof cause === "string") return redactMessage(cause); |
| | 231 | if (typeof cause !== "object" || cause === null) return errorTag(cause); |
| | 232 | |
| | 233 | const error = cause as { |
| | 234 | readonly message?: unknown; |
| | 235 | readonly code?: unknown; |
| | 236 | readonly cause?: unknown; |
| | 237 | }; |
| | 238 | const code = |
| | 239 | typeof error.code === "string" && /^[A-Z][A-Z0-9_]{0,127}$/.test(error.code) |
| | 240 | ? ` [${error.code}]` |
| | 241 | : ""; |
| | 242 | const message = typeof error.message === "string" ? `: ${redactMessage(error.message)}` : ""; |
| | 243 | const nested = |
| | 244 | error.cause === undefined ? "" : `; caused by ${summarizeCause(error.cause, depth + 1)}`; |
| | 245 | return `${errorTag(cause)}${code}${message}${nested}`; |
| | 246 | } |
| | 247 | |
| | 248 | /** Log a bounded cause summary without stacks, response payloads, or request credentials. */ |
| | 249 | export function describeUpdateErrorCause(cause: unknown): string { |
| | 250 | try { |
| | 251 | return summarizeCause(cause, 0).slice(0, 1024); |
| | 252 | } catch { |
| | 253 | return "UnknownError"; |
| | 254 | } |
| | 255 | } |
| | 256 | |
| 199 | function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYmlConfig>> { | 257 | function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYmlConfig>> { |
| 200 | const entries: Record<string, string> = {}; | 258 | const entries: Record<string, string> = {}; |
| 201 | for (const line of raw.split("\n")) { | 259 | for (const line of raw.split("\n")) { |
| ... | @@ -442,6 +500,7 @@ export const make = Effect.gen(function* () { | ... | @@ -442,6 +500,7 @@ export const make = Effect.gen(function* () { |
| 442 | ); | 500 | ); |
| 443 | yield* logUpdaterError(error.message, { | 501 | yield* logUpdaterError(error.message, { |
| 444 | errorTag: error._tag, | 502 | errorTag: error._tag, |
| | 503 | cause: describeUpdateErrorCause(error.cause), |
| 445 | channel: error.channel, | 504 | channel: error.channel, |
| 446 | }); | 505 | }); |
| 447 | return true; | 506 | return true; |
| ... | @@ -485,6 +544,7 @@ export const make = Effect.gen(function* () { | ... | @@ -485,6 +544,7 @@ export const make = Effect.gen(function* () { |
| 485 | ); | 544 | ); |
| 486 | yield* logUpdaterError(error.message, { | 545 | yield* logUpdaterError(error.message, { |
| 487 | errorTag: error._tag, | 546 | errorTag: error._tag, |
| | 547 | cause: describeUpdateErrorCause(error.cause), |
| 488 | channel: error.channel, | 548 | channel: error.channel, |
| 489 | }); | 549 | }); |
| 490 | return { accepted: true, completed: false }; | 550 | return { accepted: true, completed: false }; |
| ... | @@ -507,6 +567,7 @@ export const make = Effect.gen(function* () { | ... | @@ -507,6 +567,7 @@ export const make = Effect.gen(function* () { |
| 507 | ); | 567 | ); |
| 508 | yield* logUpdaterError(error.message, { | 568 | yield* logUpdaterError(error.message, { |
| 509 | errorTag: error._tag, | 569 | errorTag: error._tag, |
| | 570 | cause: describeUpdateErrorCause(error.cause), |
| 510 | action: error.action, | 571 | action: error.action, |
| 511 | }); | 572 | }); |
| 512 | return { accepted: true, completed: false }; | 573 | return { accepted: true, completed: false }; |
| ... | @@ -655,6 +716,7 @@ export const make = Effect.gen(function* () { | ... | @@ -655,6 +716,7 @@ export const make = Effect.gen(function* () { |
| 655 | yield* recoverFailedInstall(error.message); | 716 | yield* recoverFailedInstall(error.message); |
| 656 | yield* logUpdaterError(error.message, { | 717 | yield* logUpdaterError(error.message, { |
| 657 | errorTag: error._tag, | 718 | errorTag: error._tag, |
| | 719 | cause: describeUpdateErrorCause(error.cause), |
| 658 | channel: error.channel, | 720 | channel: error.channel, |
| 659 | isSilent: error.isSilent, | 721 | isSilent: error.isSilent, |
| 660 | isForceRunAfter: error.isForceRunAfter, | 722 | isForceRunAfter: error.isForceRunAfter, |
| ... | @@ -673,6 +735,7 @@ export const make = Effect.gen(function* () { | ... | @@ -673,6 +735,7 @@ export const make = Effect.gen(function* () { |
| 673 | yield* recoverFailedInstall(error.message); | 735 | yield* recoverFailedInstall(error.message); |
| 674 | yield* logUpdaterError(error.message, { | 736 | yield* logUpdaterError(error.message, { |
| 675 | errorTag: error._tag, | 737 | errorTag: error._tag, |
| | 738 | cause: describeUpdateErrorCause(error.cause), |
| 676 | action: error.action, | 739 | action: error.action, |
| 677 | }); | 740 | }); |
| 678 | return { accepted: true, completed: false, failed: true }; | 741 | return { accepted: true, completed: false, failed: true }; |
| ... | @@ -712,6 +775,7 @@ export const make = Effect.gen(function* () { | ... | @@ -712,6 +775,7 @@ export const make = Effect.gen(function* () { |
| 712 | const error = new DesktopUpdatePollerError({ poller: "startup", cause }); | 775 | const error = new DesktopUpdatePollerError({ poller: "startup", cause }); |
| 713 | return logUpdaterError(error.message, { | 776 | return logUpdaterError(error.message, { |
| 714 | errorTag: error._tag, | 777 | errorTag: error._tag, |
| | 778 | cause: describeUpdateErrorCause(error.cause), |
| 715 | poller: error.poller, | 779 | poller: error.poller, |
| 716 | }); | 780 | }); |
| 717 | }), | 781 | }), |
| ... | @@ -727,6 +791,7 @@ export const make = Effect.gen(function* () { | ... | @@ -727,6 +791,7 @@ export const make = Effect.gen(function* () { |
| 727 | const error = new DesktopUpdatePollerError({ poller: "poll", cause }); | 791 | const error = new DesktopUpdatePollerError({ poller: "poll", cause }); |
| 728 | return logUpdaterError(error.message, { | 792 | return logUpdaterError(error.message, { |
| 729 | errorTag: error._tag, | 793 | errorTag: error._tag, |
| | 794 | cause: describeUpdateErrorCause(error.cause), |
| 730 | poller: error.poller, | 795 | poller: error.poller, |
| 731 | }); | 796 | }); |
| 732 | }), | 797 | }), |
| ... | @@ -782,6 +847,7 @@ export const make = Effect.gen(function* () { | ... | @@ -782,6 +847,7 @@ export const make = Effect.gen(function* () { |
| 782 | const error = new DesktopUpdateEventHandlingError({ event: "update-available", cause }); | 847 | const error = new DesktopUpdateEventHandlingError({ event: "update-available", cause }); |
| 783 | return logUpdaterWarning(error.message, { | 848 | return logUpdaterWarning(error.message, { |
| 784 | errorTag: error._tag, | 849 | errorTag: error._tag, |
| | 850 | cause: describeUpdateErrorCause(error.cause), |
| 785 | event: error.event, | 851 | event: error.event, |
| 786 | }); | 852 | }); |
| 787 | }), | 853 | }), |
| ... | @@ -811,6 +877,7 @@ export const make = Effect.gen(function* () { | ... | @@ -811,6 +877,7 @@ export const make = Effect.gen(function* () { |
| 811 | yield* recoverFailedInstall(error.message); | 877 | yield* recoverFailedInstall(error.message); |
| 812 | yield* logUpdaterError(error.message, { | 878 | yield* logUpdaterError(error.message, { |
| 813 | errorTag: error._tag, | 879 | errorTag: error._tag, |
| | 880 | cause: describeUpdateErrorCause(error.cause), |
| 814 | operation: error.operation, | 881 | operation: error.operation, |
| 815 | }); | 882 | }); |
| 816 | return; | 883 | return; |
| ... | @@ -831,6 +898,7 @@ export const make = Effect.gen(function* () { | ... | @@ -831,6 +898,7 @@ export const make = Effect.gen(function* () { |
| 831 | | 898 | |
| 832 | yield* logUpdaterError(error.message, { | 899 | yield* logUpdaterError(error.message, { |
| 833 | errorTag: error._tag, | 900 | errorTag: error._tag, |
| | 901 | cause: describeUpdateErrorCause(error.cause), |
| 834 | operation: error.operation, | 902 | operation: error.operation, |
| 835 | }); | 903 | }); |
| 836 | }); | 904 | }); |
| ... | @@ -861,6 +929,7 @@ export const make = Effect.gen(function* () { | ... | @@ -861,6 +929,7 @@ export const make = Effect.gen(function* () { |
| 861 | const error = new DesktopUpdateEventHandlingError({ event: "download-progress", cause }); | 929 | const error = new DesktopUpdateEventHandlingError({ event: "download-progress", cause }); |
| 862 | return logUpdaterWarning(error.message, { | 930 | return logUpdaterWarning(error.message, { |
| 863 | errorTag: error._tag, | 931 | errorTag: error._tag, |
| | 932 | cause: describeUpdateErrorCause(error.cause), |
| 864 | event: error.event, | 933 | event: error.event, |
| 865 | }); | 934 | }); |
| 866 | }), | 935 | }), |
| ... | @@ -885,6 +954,7 @@ export const make = Effect.gen(function* () { | ... | @@ -885,6 +954,7 @@ export const make = Effect.gen(function* () { |
| 885 | const error = new DesktopUpdateEventHandlingError({ event: "update-downloaded", cause }); | 954 | const error = new DesktopUpdateEventHandlingError({ event: "update-downloaded", cause }); |
| 886 | return logUpdaterWarning(error.message, { | 955 | return logUpdaterWarning(error.message, { |
| 887 | errorTag: error._tag, | 956 | errorTag: error._tag, |
| | 957 | cause: describeUpdateErrorCause(error.cause), |
| 888 | event: error.event, | 958 | event: error.event, |
| 889 | }); | 959 | }); |
| 890 | }), | 960 | }), |