diff --git a/web/netlify/functions/update-settings.ts b/web/netlify/functions/update-settings.ts index 5ae1ea5fb..fb41d9d3d 100644 --- a/web/netlify/functions/update-settings.ts +++ b/web/netlify/functions/update-settings.ts @@ -4,6 +4,10 @@ import { createClient } from "@supabase/supabase-js"; import { Database } from "../../src/types/supabase-notification"; import messages from "../../src/consts/eip712-messages"; import { EMAIL_REGEX, TELEGRAM_REGEX, ETH_ADDRESS_REGEX, ETH_SIGNATURE_REGEX } from "../../src/consts/index"; +import { createLogger, throwNewError } from "../../src/utils/logger"; +import dotenv from "dotenv"; + +dotenv.config(); type NotificationSettings = { email?: string; @@ -13,12 +17,15 @@ type NotificationSettings = { signature: string; }; +const logger = createLogger(process.env.LOGTAIL_TOKEN).child({ function: "update-settings" }); +const logAndThrowNewError = (message: string, error?: any) => throwNewError(logger, message, error); + const parse = (inputString: string): NotificationSettings => { let input; try { input = JSON.parse(inputString); } catch (err) { - throw new Error("Invalid JSON format"); + logAndThrowNewError("Invalid JSON format", err); } const requiredKeys: (keyof NotificationSettings)[] = ["nonce", "address", "signature"]; @@ -27,37 +34,37 @@ const parse = (inputString: string): NotificationSettings => { for (const key of requiredKeys) { if (!receivedKeys.includes(key)) { - throw new Error(`Missing key: ${key}`); + logAndThrowNewError(`Missing key: ${key}`); } } const allExpectedKeys = [...requiredKeys, ...optionalKeys]; for (const key of receivedKeys) { if (!allExpectedKeys.includes(key as keyof NotificationSettings)) { - throw new Error(`Unexpected key: ${key}`); + logAndThrowNewError(`Unexpected key: ${key}`); } } const email = input.email ? input.email.trim() : ""; if (email && !EMAIL_REGEX.test(email)) { - throw new Error("Invalid email format"); + logAndThrowNewError("Invalid email format"); } const telegram = input.telegram ? input.telegram.trim() : ""; if (telegram && !TELEGRAM_REGEX.test(telegram)) { - throw new Error("Invalid Telegram username format"); + logAndThrowNewError("Invalid Telegram username format"); } if (!/^\d+$/.test(input.nonce)) { - throw new Error("Invalid nonce format. Expected an integer as a string."); + logAndThrowNewError("Invalid nonce format. Expected an integer as a string."); } if (!ETH_ADDRESS_REGEX.test(input.address)) { - throw new Error("Invalid Ethereum address format"); + logAndThrowNewError("Invalid Ethereum address format"); } if (!ETH_SIGNATURE_REGEX.test(input.signature)) { - throw new Error("Invalid signature format"); + logAndThrowNewError("Invalid signature format"); } return { @@ -72,7 +79,7 @@ const parse = (inputString: string): NotificationSettings => { export const handler: Handler = async (event) => { try { if (!event.body) { - throw new Error("No body provided"); + logAndThrowNewError("No body provided"); } const { email, telegram, nonce, address, signature } = parse(event.body); const lowerCaseAddress = address.toLowerCase() as `0x${string}`; @@ -85,6 +92,7 @@ export const handler: Handler = async (event) => { }); if (!isValid) { // If the recovered address does not match the provided address, return an error + logAndThrowNewError("Signature verification failed"); throw new Error("Signature verification failed"); } @@ -94,6 +102,7 @@ export const handler: Handler = async (event) => { if (email === "" && telegram === "") { const { error } = await supabase.from("users").delete().match({ address: lowerCaseAddress }); if (error) throw error; + logger.info("Record deleted successfully."); return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) }; } @@ -105,8 +114,12 @@ export const handler: Handler = async (event) => { if (error) { throw error; } + logger.info("Record updated successfully."); return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) }; } catch (err) { + logger.error(err); return { statusCode: 500, body: JSON.stringify({ message: `Error: ${err}` }) }; + } finally { + logger.flush(); } }; diff --git a/web/package.json b/web/package.json index a0e8e8a4a..e8cea341a 100644 --- a/web/package.json +++ b/web/package.json @@ -70,6 +70,7 @@ "@filebase/client": "^0.0.5", "@kleros/kleros-v2-contracts": "workspace:^", "@kleros/ui-components-library": "^2.6.2", + "@logtail/pino": "^0.4.12", "@sentry/react": "^7.55.2", "@sentry/tracing": "^7.55.2", "@supabase/supabase-js": "^2.33.1", @@ -87,6 +88,8 @@ "moment": "^2.29.4", "overlayscrollbars": "^2.3.0", "overlayscrollbars-react": "^0.5.2", + "pino": "^8.16.0", + "pino-pretty": "^10.2.3", "react": "^18.2.0", "react-chartjs-2": "^4.3.1", "react-dom": "^18.2.0", diff --git a/web/src/utils/logger.ts b/web/src/utils/logger.ts new file mode 100644 index 000000000..1bea53f77 --- /dev/null +++ b/web/src/utils/logger.ts @@ -0,0 +1,40 @@ +import pino, { TransportTargetOptions } from "pino"; + +// Intended for Netlify functions +export const createLogger = (logtailToken?: string): pino.Logger => { + const targets: TransportTargetOptions[] = [ + { + target: "pino-pretty", + options: {}, + level: "info", + }, + ]; + if (logtailToken) { + targets.push({ + target: "@logtail/pino", + options: { sourceToken: logtailToken }, + level: "info", + }); + } + return pino( + { + level: "info", + timestamp: pino.stdTimeFunctions.isoTime, + }, + pino.transport({ targets: targets }) + ); +}; + +export const throwNewError = (logger: pino.Logger, message: string, error?: any) => { + if (!error) { + logger.error(message); + throw new Error(message); + } + if (typeof error === "string") { + logger.error(error, message); + throw new Error(message + ": " + error); + } else if (error instanceof Error) { + logger.error(error, message); + throw new Error(message + ": " + error.name + ": " + error.message); + } +}; diff --git a/yarn.lock b/yarn.lock index 7aa3ea561..eb99f59dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5365,6 +5365,7 @@ __metadata: "@kleros/kleros-v2-prettier-config": "workspace:^" "@kleros/kleros-v2-tsconfig": "workspace:^" "@kleros/ui-components-library": ^2.6.2 + "@logtail/pino": ^0.4.12 "@netlify/functions": ^1.6.0 "@parcel/transformer-svg-react": 2.8.3 "@parcel/watcher": ~2.2.0 @@ -5401,6 +5402,8 @@ __metadata: overlayscrollbars: ^2.3.0 overlayscrollbars-react: ^0.5.2 parcel: 2.8.3 + pino: ^8.16.0 + pino-pretty: ^10.2.3 react: ^18.2.0 react-chartjs-2: ^4.3.1 react-dom: ^18.2.0 @@ -5663,6 +5666,16 @@ __metadata: languageName: node linkType: hard +"@logtail/core@npm:^0.4.12": + version: 0.4.12 + resolution: "@logtail/core@npm:0.4.12" + dependencies: + "@logtail/tools": ^0.4.12 + "@logtail/types": ^0.4.11 + checksum: 615874d456ed732d650d4a022bd14e2a400198b26dfd07cb512a365e8369a8ab1d48b74657c7bc1a77e03b430778f652d41c84638c3efe9de2ef91cfe6f4dd29 + languageName: node + linkType: hard + "@logtail/node@npm:^0.4.0": version: 0.4.0 resolution: "@logtail/node@npm:0.4.0" @@ -5678,6 +5691,21 @@ __metadata: languageName: node linkType: hard +"@logtail/node@npm:^0.4.12": + version: 0.4.12 + resolution: "@logtail/node@npm:0.4.12" + dependencies: + "@logtail/core": ^0.4.12 + "@logtail/types": ^0.4.11 + "@msgpack/msgpack": ^2.5.1 + "@types/stack-trace": ^0.0.29 + cross-fetch: ^3.0.4 + minimatch: ^3.0.4 + stack-trace: ^0.0.10 + checksum: d2799eb561e4bdee4fb9875350983cb5f7ede2ab6c368bf3673cbcfdc7df3c16616d6b881edf60dfe64e0bafc6c6a63d96455bb0a4d34328ed3414fd94d47c5f + languageName: node + linkType: hard + "@logtail/pino@npm:^0.4.0": version: 0.4.0 resolution: "@logtail/pino@npm:0.4.0" @@ -5691,6 +5719,19 @@ __metadata: languageName: node linkType: hard +"@logtail/pino@npm:^0.4.12": + version: 0.4.12 + resolution: "@logtail/pino@npm:0.4.12" + dependencies: + "@logtail/node": ^0.4.12 + "@logtail/types": ^0.4.11 + pino-abstract-transport: ^1.0.0 + peerDependencies: + pino: ^7.0.0 || ^8.0.0 + checksum: 1b6ee658ad5e60c65cc0bd50aaf1e99b26150e2b26622a13c09fcb4e15f33dd8979aa58401d06524d82a5ea19511c96871bbdb57aaa4166586dc76159b01110e + languageName: node + linkType: hard + "@logtail/tools@npm:^0.4.0": version: 0.4.0 resolution: "@logtail/tools@npm:0.4.0" @@ -5700,6 +5741,15 @@ __metadata: languageName: node linkType: hard +"@logtail/tools@npm:^0.4.12": + version: 0.4.12 + resolution: "@logtail/tools@npm:0.4.12" + dependencies: + "@logtail/types": ^0.4.11 + checksum: b0a8391a763b7f7f7d889446ed857715f158288f615b089f125471582efd8bb2a9525e37bb58f88f166224529ce3bc74799807ff768905e79ac1c92d5877aa1e + languageName: node + linkType: hard + "@logtail/types@npm:^0.4.0": version: 0.4.0 resolution: "@logtail/types@npm:0.4.0" @@ -5709,6 +5759,15 @@ __metadata: languageName: node linkType: hard +"@logtail/types@npm:^0.4.11": + version: 0.4.11 + resolution: "@logtail/types@npm:0.4.11" + dependencies: + js: ^0.1.0 + checksum: 0102a079d0ba89efc70770f6d853f0b205784a73abe570ab056f830d4ab031f95a1d80e2a3dfa54ebf53193d20fd9b61d52c2be03032d28572ce9fcb3be89144 + languageName: node + linkType: hard + "@metamask/eth-sig-util@npm:^4.0.0": version: 4.0.1 resolution: "@metamask/eth-sig-util@npm:4.0.1" @@ -24731,6 +24790,16 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:v1.1.0": + version: 1.1.0 + resolution: "pino-abstract-transport@npm:1.1.0" + dependencies: + readable-stream: ^4.0.0 + split2: ^4.0.0 + checksum: cc84caabee5647b5753ae484d5f63a1bca0f6e1791845e2db2b6d830a561c2b5dd1177720f68d78994c8a93aecc69f2729e6ac2bc871a1bf5bb4b0ec17210668 + languageName: node + linkType: hard + "pino-pretty@npm:^10.0.0": version: 10.0.0 resolution: "pino-pretty@npm:10.0.0" @@ -24755,6 +24824,30 @@ __metadata: languageName: node linkType: hard +"pino-pretty@npm:^10.2.3": + version: 10.2.3 + resolution: "pino-pretty@npm:10.2.3" + dependencies: + colorette: ^2.0.7 + dateformat: ^4.6.3 + fast-copy: ^3.0.0 + fast-safe-stringify: ^2.1.1 + help-me: ^4.0.1 + joycon: ^3.1.1 + minimist: ^1.2.6 + on-exit-leak-free: ^2.1.0 + pino-abstract-transport: ^1.0.0 + pump: ^3.0.0 + readable-stream: ^4.0.0 + secure-json-parse: ^2.4.0 + sonic-boom: ^3.0.0 + strip-json-comments: ^3.1.1 + bin: + pino-pretty: bin.js + checksum: 9182886855515000df2ef381762c69fc29dbdd9014a76839cc3d8a7a94ac96d4ce17423adb9ddd61eae78986bb0ff3a1d9e6e7aa55476c096a3dd4a0c89440e8 + languageName: node + linkType: hard + "pino-std-serializers@npm:^4.0.0": version: 4.0.0 resolution: "pino-std-serializers@npm:4.0.0" @@ -24811,6 +24904,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:^8.16.0": + version: 8.16.0 + resolution: "pino@npm:8.16.0" + dependencies: + atomic-sleep: ^1.0.0 + fast-redact: ^3.1.1 + on-exit-leak-free: ^2.1.0 + pino-abstract-transport: v1.1.0 + pino-std-serializers: ^6.0.0 + process-warning: ^2.0.0 + quick-format-unescaped: ^4.0.3 + real-require: ^0.2.0 + safe-stable-stringify: ^2.3.1 + sonic-boom: ^3.7.0 + thread-stream: ^2.0.0 + bin: + pino: bin.js + checksum: c3af0d1d80d0a7ec59530e6c3668895ac813762829ea0b7e316057370f58011d09e128e67289665652904367a1f27f87cca4e564eb3ff2a0d46219f12fcf896e + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -28387,6 +28501,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^3.7.0": + version: 3.7.0 + resolution: "sonic-boom@npm:3.7.0" + dependencies: + atomic-sleep: ^1.0.0 + checksum: 528f0f7f7e09dcdb02ad5985039f66554266cbd8813f9920781607c9248e01f468598c1334eab2cc740c016a63c8b2a20e15c3f618cddb08ea1cfb4a390a796e + languageName: node + linkType: hard + "source-list-map@npm:^2.0.0, source-list-map@npm:^2.0.1": version: 2.0.1 resolution: "source-list-map@npm:2.0.1"