diff --git a/package.json b/package.json index ad237cf..771f1b6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", + "@supabase/supabase-js": "^2.45.3", "@types/jest": "^29.5.12", "@types/node": "^20.10.4", "@typescript-eslint/eslint-plugin": "^7.14.1", @@ -67,6 +68,5 @@ "extends": [ "@commitlint/config-conventional" ] - }, - "packageManager": "yarn@1.22.22" + } } diff --git a/src/logs.ts b/src/logs.ts index c9d8214..578f8f1 100644 --- a/src/logs.ts +++ b/src/logs.ts @@ -1,10 +1,42 @@ +import { SupabaseClient } from "@supabase/supabase-js"; import { LOG_LEVEL } from "./constants"; import { PrettyLogs } from "./pretty-logs"; import { LogParams, LogReturn, Metadata, LogLevel } from "./types/log-types"; +type SupabaseConfig = { + supabaseClient: SupabaseClient; + levelsToLog: LogLevel[]; +}; + export class Logs { private _maxLevel = -1; static console: PrettyLogs; + private _supabase: SupabaseClient | null = null; + private _pluginName: string; + private _levelsToLog: LogLevel[] = []; + + constructor(level: LogLevel, pluginName: string, postingConfig?: SupabaseConfig) { + this._log = this._log.bind(this); + this._logToSupabase = this._logToSupabase.bind(this); + this._diffColorCommentMessage = this._diffColorCommentMessage.bind(this); + this._getNumericLevel = this._getNumericLevel.bind(this); + this.ok = this.ok.bind(this); + this.info = this.info.bind(this); + this.error = this.error.bind(this); + this.debug = this.debug.bind(this); + this.fatal = this.fatal.bind(this); + this.verbose = this.verbose.bind(this); + this._addDiagnosticInformation = this._addDiagnosticInformation.bind(this); + + this._maxLevel = this._getNumericLevel(level); + Logs.console = new PrettyLogs(); + this._pluginName = pluginName; + + if (postingConfig) { + this._levelsToLog = postingConfig.levelsToLog; + this._supabase = postingConfig.supabaseClient; + } + } private _log({ level, consoleLog, logMessage, metadata, type }: LogParams): LogReturn { // filter out more verbose logs according to maxLevel set in config @@ -12,7 +44,7 @@ export class Logs { consoleLog(logMessage, metadata); } - return new LogReturn( + const log = new LogReturn( { raw: logMessage, diff: this._diffColorCommentMessage(type, logMessage), @@ -21,6 +53,12 @@ export class Logs { }, metadata ); + + if (this._supabase && this._levelsToLog.includes(level)) { + this._logToSupabase(log).catch((error) => this.error(error.message, { error })); + } + + return log; } private _addDiagnosticInformation(metadata?: Metadata) { @@ -92,14 +130,14 @@ export class Logs { public fatal(log: string, metadata?: Metadata): LogReturn { if (!metadata) { - metadata = Logs.convertErrorsIntoObjects(new Error(log)) as Metadata; + metadata = Logs.convertErrorsIntoObjects(new Error(log)); const stack = metadata.stack as string[]; stack.splice(1, 1); metadata.stack = stack; } if (metadata instanceof Error) { - metadata = Logs.convertErrorsIntoObjects(metadata) as Metadata; + metadata = Logs.convertErrorsIntoObjects(metadata); const stack = metadata.stack as string[]; stack.splice(1, 1); metadata.stack = stack; @@ -126,12 +164,13 @@ export class Logs { }); } - constructor(logLevel: LogLevel) { - this._maxLevel = this._getNumericLevel(logLevel); - Logs.console = new PrettyLogs(); - } - private _diffColorCommentMessage(type: string, message: string) { + if (!message) { + return ""; + } + if (typeof message !== "string") { + message = JSON.stringify(message, null, 2); + } const diffPrefix = { fatal: "-", // - text in red ok: "+", // + text in green @@ -183,7 +222,8 @@ export class Logs { return -1; } } - static convertErrorsIntoObjects(obj: unknown): Metadata | unknown { + + static convertErrorsIntoObjects(obj: unknown): Metadata { // this is a utility function to render native errors in the console, the database, and on GitHub. if (obj instanceof Error) { return { @@ -197,6 +237,28 @@ export class Logs { obj[key] = this.convertErrorsIntoObjects(obj[key]); }); } - return obj; + return obj as Metadata; + } + + private async _logToSupabase(log: LogReturn) { + if (!this._supabase) { + return; + } + try { + const { data, error } = await this._supabase.from("logs").insert([ + { + log: log.logMessage.raw, + level: log.logMessage.level, + metadata: { ...log.metadata, caller: this._pluginName }, + }, + ]); + if (error) { + throw error; + } + return data; + } catch (err) { + console.error("Error logging to Supabase:", err); + throw err; + } } } diff --git a/src/types/log-types.ts b/src/types/log-types.ts index 6f25618..78210e3 100644 --- a/src/types/log-types.ts +++ b/src/types/log-types.ts @@ -25,11 +25,12 @@ interface MetadataInterface { export type Metadata = Partial; -export class LogReturn { +export class LogReturn extends Error { logMessage: LogMessage; metadata?: Metadata; constructor(logMessage: LogMessage, metadata?: Metadata) { + super(logMessage.raw); this.logMessage = logMessage; this.metadata = metadata; } diff --git a/tests/logs.test.ts b/tests/logs.test.ts index 73a745f..eed6740 100644 --- a/tests/logs.test.ts +++ b/tests/logs.test.ts @@ -1,12 +1,25 @@ +import { SupabaseClient } from "@supabase/supabase-js"; import { LOG_LEVEL } from "../src/constants"; import { Logs } from "../src/logs"; import { LogReturn } from "../src/types/log-types"; +jest.mock("@supabase/supabase-js", () => { + return { + SupabaseClient: jest.fn().mockImplementation(() => { + return { + from: jest.fn().mockImplementation(() => { + throw new Error("This is the error you are looking for."); + }), + }; + }), + }; +}); + describe("Logs", () => { let logs: Logs; beforeEach(() => { - logs = new Logs(LOG_LEVEL.DEBUG); + logs = new Logs(LOG_LEVEL.DEBUG, "test"); }); it("should log an 'ok' message", () => { @@ -47,4 +60,15 @@ describe("Logs", () => { expect(msg).toHaveProperty("diff"); expect(metadata).toHaveProperty("caller"); }); + + it("should log an error when _logToSupabase throws an error", async () => { + const supabaseClient = new SupabaseClient("test", "test"); + const logger = new Logs(LOG_LEVEL.DEBUG, "test", { levelsToLog: ["fatal"], supabaseClient }); + const spy = jest.spyOn(console, "error"); + logger.fatal("This is not the error you are looking for"); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenLastCalledWith("Error logging to Supabase:", new Error("This is the error you are looking for.")); + }); }); diff --git a/yarn.lock b/yarn.lock index dd104c6..f4cd1a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,63 @@ ignore "^5.1.8" p-map "^4.0.0" +"@supabase/auth-js@2.65.0": + version "2.65.0" + resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.65.0.tgz#e345c492f8cbc31cd6289968eae0e349ff0f39e9" + integrity sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/functions-js@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.1.tgz#373e75f8d3453bacd71fb64f88d7a341d7b53ad7" + integrity sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": + version "2.6.15" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" + integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/postgrest-js@1.15.8": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.8.tgz#827aaa408cdbc89e67d0a758e7a545ac86e34312" + integrity sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/realtime-js@2.10.2": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.10.2.tgz#c2b42d17d723d2d2a9146cfad61dc3df1ce3127e" + integrity sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@types/phoenix" "^1.5.4" + "@types/ws" "^8.5.10" + ws "^8.14.2" + +"@supabase/storage-js@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.7.0.tgz#9ff322d2c3b141087aa34115cf14205e4980ce75" + integrity sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/supabase-js@^2.45.3": + version "2.45.3" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.45.3.tgz#d32b7a7b379958a10dcce32af1182f82b5b82218" + integrity sha512-4wAux6cuVMrdH/qUjKn6p3p3L9AtAO3Une6ojIrtpCj1RaXKVoyIATiacSRAI+pKff6XZBVCGC29v+z4Jo/uSw== + dependencies: + "@supabase/auth-js" "2.65.0" + "@supabase/functions-js" "2.4.1" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.15.8" + "@supabase/realtime-js" "2.10.2" + "@supabase/storage-js" "2.7.0" + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -1434,11 +1491,23 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +"@types/phoenix@^1.5.4": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.5.tgz#5654e14ec7ad25334a157a20015996b6d7d2075e" + integrity sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w== + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/ws@^8.5.10": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -5522,6 +5591,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -5810,11 +5884,24 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -5914,6 +6001,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.14.2: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"