diff --git a/.env.sample b/.env.sample index 9d5bef133..9f5e894de 100644 --- a/.env.sample +++ b/.env.sample @@ -65,4 +65,8 @@ DATABASE_URL="postgresql://sn:password@db:5432/stackernews?schema=public" # postgres container stuff POSTGRES_PASSWORD=password POSTGRES_USER=sn -POSTGRES_DB=stackernews \ No newline at end of file +POSTGRES_DB=stackernews + +# slack +SLACK_BOT_TOKEN= +SLACK_CHANNEL_ID= diff --git a/api/slack/index.js b/api/slack/index.js new file mode 100644 index 000000000..cd1f220db --- /dev/null +++ b/api/slack/index.js @@ -0,0 +1,17 @@ +import { WebClient, LogLevel } from '@slack/web-api' + +const slackClient = global.slackClient || (() => { + if (!process.env.SLACK_BOT_TOKEN && !process.env.SLACK_CHANNEL_ID) { + console.warn('SLACK_* env vars not set, skipping slack setup') + return null + } + console.log('initing slack client') + const client = new WebClient(process.env.SLACK_BOT_TOKEN, { + logLevel: LogLevel.INFO + }) + return client +})() + +if (process.env.NODE_ENV === 'development') global.slackClient = slackClient + +export default slackClient diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 3cb97a9f6..82674c364 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -25,7 +25,7 @@ export default gql` noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, clickToLoadImg: Boolean!, wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!], hideBookmarks: Boolean!, - noteForwardedSats: Boolean!, hideWalletBalance: Boolean!, hideIsContributor: Boolean!): User + noteForwardedSats: Boolean!, hideWalletBalance: Boolean!, hideIsContributor: Boolean!, diagnostics: Boolean!): User setPhoto(photoId: ID!): Int! upsertBio(bio: String!): User! setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean @@ -87,6 +87,7 @@ export default gql` hideBookmarks: Boolean! hideWelcomeBanner: Boolean! hideWalletBalance: Boolean! + diagnostics: Boolean! clickToLoadImg: Boolean! wildWestMode: Boolean! greeterMode: Boolean! diff --git a/components/logger.js b/components/logger.js new file mode 100644 index 000000000..1c86962a5 --- /dev/null +++ b/components/logger.js @@ -0,0 +1,109 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useMe } from './me' +import fancyNames from '../lib/fancy-names.json' + +const generateFancyName = () => { + // 100 adjectives * 100 nouns * 10000 = 100M possible names + const pickRandom = (array) => array[Math.floor(Math.random() * array.length)] + const adj = pickRandom(fancyNames.adjectives) + const noun = pickRandom(fancyNames.nouns) + const id = Math.floor(Math.random() * fancyNames.maxSuffix) + return `${adj}-${noun}-${id}` +} + +function detectOS () { + if (!window.navigator) return '' + + const userAgent = window.navigator.userAgent + const platform = window.navigator.userAgentData?.platform || window.navigator.platform + const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'] + const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'] + const iosPlatforms = ['iPhone', 'iPad', 'iPod'] + let os = null + + if (macosPlatforms.indexOf(platform) !== -1) { + os = 'Mac OS' + } else if (iosPlatforms.indexOf(platform) !== -1) { + os = 'iOS' + } else if (windowsPlatforms.indexOf(platform) !== -1) { + os = 'Windows' + } else if (/Android/.test(userAgent)) { + os = 'Android' + } else if (/Linux/.test(platform)) { + os = 'Linux' + } + + return os +} + +const LoggerContext = createContext() + +export function LoggerProvider ({ children }) { + const me = useMe() + const [name, setName] = useState() + const [os, setOS] = useState() + + useEffect(() => { + let name = window.localStorage.getItem('fancy-name') + if (!name) { + name = generateFancyName() + window.localStorage.setItem('fancy-name', name) + } + setName(name) + setOS(detectOS()) + }, []) + + const log = useCallback(level => { + return async (message, context) => { + if (!me || !me.diagnostics) return + const env = { + userAgent: window.navigator.userAgent, + // os may not be initialized yet + os: os || detectOS() + } + const body = { + level, + env, + // name may be undefined if it wasn't stored in local storage yet + // we fallback to local storage since on page reloads, the name may wasn't fetched from local storage yet + name: name || window.localStorage.getItem('fancy-name'), + message, + context + } + await fetch('/api/log', { + method: 'post', + headers: { + 'Content-type': 'application/json' + }, + body: JSON.stringify(body) + }).catch(console.error) + } + }, [me?.diagnostics, name, os]) + + const logger = useMemo(() => ({ + info: log('info'), + warn: log('warn'), + error: log('error'), + name + }), [log, name]) + + useEffect(() => { + // for communication between app and service worker + const channel = new MessageChannel() + navigator?.serviceWorker?.controller?.postMessage({ action: 'MESSAGE_PORT' }, [channel.port2]) + channel.port1.onmessage = (event) => { + const { message, level, context } = Object.assign({ level: 'info' }, event.data) + logger[level](message, context) + } + }, [logger]) + + return ( + + {children} + + ) +} + +export function useLogger () { + return useContext(LoggerContext) +} diff --git a/components/serviceworker.js b/components/serviceworker.js index f6cb03684..06b25cb4a 100644 --- a/components/serviceworker.js +++ b/components/serviceworker.js @@ -1,6 +1,7 @@ import { createContext, useContext, useEffect, useState, useCallback } from 'react' import { Workbox } from 'workbox-window' import { gql, useMutation } from '@apollo/client' +import { useLogger } from './logger' const applicationServerKey = process.env.NEXT_PUBLIC_VAPID_PUBKEY @@ -34,6 +35,7 @@ export const ServiceWorkerProvider = ({ children }) => { } } `) + const logger = useLogger() // I am not entirely sure if this is needed since at least in Brave, // using `registration.pushManager.subscribe` also prompts the user. @@ -60,6 +62,8 @@ export const ServiceWorkerProvider = ({ children }) => { // Brave users must enable a flag in brave://settings/privacy first // see https://stackoverflow.com/a/69624651 let pushSubscription = await registration.pushManager.subscribe(subscribeOptions) + const { endpoint } = pushSubscription + logger.info('subscribed to push notifications', { endpoint }) // convert keys from ArrayBuffer to string pushSubscription = JSON.parse(JSON.stringify(pushSubscription)) // Send subscription to service worker to save it so we can use it later during `pushsubscriptionchange` @@ -68,24 +72,30 @@ export const ServiceWorkerProvider = ({ children }) => { action: 'STORE_SUBSCRIPTION', subscription: pushSubscription }) + logger.info('sent STORE_SUBSCRIPTION to service worker', { endpoint }) // send subscription to server const variables = { - endpoint: pushSubscription.endpoint, + endpoint, p256dh: pushSubscription.keys.p256dh, auth: pushSubscription.keys.auth } await savePushSubscription({ variables }) + logger.info('sent push subscription to server', { endpoint }) } const unsubscribeFromPushNotifications = async (subscription) => { await subscription.unsubscribe() const { endpoint } = subscription + logger.info('unsubscribed from push notifications', { endpoint }) await deletePushSubscription({ variables: { endpoint } }) + logger.info('deleted push subscription from server', { endpoint }) } const togglePushSubscription = useCallback(async () => { const pushSubscription = await registration.pushManager.getSubscription() - if (pushSubscription) return unsubscribeFromPushNotifications(pushSubscription) + if (pushSubscription) { + return unsubscribeFromPushNotifications(pushSubscription) + } return subscribeToPushNotifications() }) @@ -100,12 +110,17 @@ export const ServiceWorkerProvider = ({ children }) => { // we sync with server manually by checking on every page reload if the push subscription changed. // see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f navigator?.serviceWorker?.controller?.postMessage?.({ action: 'SYNC_SUBSCRIPTION' }) + logger.info('sent SYNC_SUBSCRIPTION to service worker') }, []) useEffect(() => { - if (!support.serviceWorker) return + if (!support.serviceWorker) { + logger.info('device does not support service worker') + return + } const wb = new Workbox('/sw.js', { scope: '/' }) wb.register().then(registration => { + logger.info('service worker registration successful') setRegistration(registration) }) }, [support.serviceWorker]) diff --git a/fragments/users.js b/fragments/users.js index 6871ceac5..1860ab16a 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -31,6 +31,7 @@ export const ME = gql` hideFromTopUsers hideCowboyHat clickToLoadImg + diagnostics wildWestMode greeterMode lastCheckedJobs @@ -62,6 +63,7 @@ export const SETTINGS_FIELDS = gql` hideIsContributor clickToLoadImg hideWalletBalance + diagnostics nostrPubkey nostrRelays wildWestMode @@ -91,14 +93,14 @@ mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $clickToLoadImg: Boolean!, $wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!], $hideBookmarks: Boolean!, - $noteForwardedSats: Boolean!, $hideWalletBalance: Boolean!, $hideIsContributor: Boolean!) { + $noteForwardedSats: Boolean!, $hideWalletBalance: Boolean!, $hideIsContributor: Boolean!, $diagnostics: Boolean!) { setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, clickToLoadImg: $clickToLoadImg, wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays, hideBookmarks: $hideBookmarks, - noteForwardedSats: $noteForwardedSats, hideWalletBalance: $hideWalletBalance, hideIsContributor: $hideIsContributor) { + noteForwardedSats: $noteForwardedSats, hideWalletBalance: $hideWalletBalance, hideIsContributor: $hideIsContributor, diagnostics: $diagnostics) { ...SettingsFields } } diff --git a/lib/fancy-names.json b/lib/fancy-names.json new file mode 100644 index 000000000..9ecc3f8f9 --- /dev/null +++ b/lib/fancy-names.json @@ -0,0 +1,207 @@ +{ + "adjectives": [ + "mighty", + "radiant", + "whimsical", + "cosmic", + "enchanted", + "electric", + "serene", + "vibrant", + "fierce", + "mystical", + "playful", + "daring", + "soothing", + "galactic", + "exuberant", + "harmonious", + "energetic", + "tranquil", + "sparkling", + "majestic", + "luminous", + "brave", + "blissful", + "captivating", + "ethereal", + "dynamic", + "spirited", + "graceful", + "magical", + "adventurous", + "resplendent", + "serendipitous", + "tenacious", + "whirlwind", + "jubilant", + "enigmatic", + "mystifying", + "zephyr", + "celestial", + "enthralling", + "curious", + "infinite", + "radiant", + "mesmerizing", + "vibrant", + "euphoric", + "awe-inspiring", + "phenomenal", + "serene", + "bewitching", + "impulsive", + "thrilling", + "eclectic", + "vivacious", + "spirited", + "enchanting", + "dynamic", + "brilliant", + "harmonic", + "charismatic", + "courageous", + "tenacious", + "lively", + "bewildering", + "whimsical", + "enveloping", + "playful", + "captivating", + "inspiring", + "zenith", + "majestic", + "dazzling", + "resilient", + "celestial", + "resplendent", + "enigmatic", + "tranquil", + "ethereal", + "exquisite", + "radiating", + "breathtaking", + "rhapsodic", + "melodic", + "phenomenal", + "enchanted", + "invincible", + "serendipitous", + "kaleidoscopic", + "intriguing", + "spellbinding", + "thriving", + "thrilling", + "reverie", + "exhilarating", + "invigorating", + "resolute", + "audacious", + "empowering", + "jubilant", + "timeless" + ], + "nouns": [ + "summer", + "phoenix", + "echo", + "voyager", + "starlight", + "harmony", + "nova", + "dreamer", + "cascade", + "celestial", + "dragon", + "whisper", + "serenade", + "avalanche", + "pinnacle", + "odyssey", + "enigma", + "zenith", + "mirage", + "symphony", + "nebula", + "infinity", + "serenity", + "radiance", + "horizon", + "eclipse", + "solstice", + "aria", + "tornado", + "aurora", + "mirage", + "quasar", + "cascade", + "seraph", + "nebula", + "solitude", + "paradigm", + "infinity", + "melody", + "nebula", + "radiance", + "odyssey", + "seraph", + "melody", + "aria", + "zenith", + "eclipse", + "tornado", + "solstice", + "celestia", + "phoenix", + "voyager", + "starlight", + "dreamer", + "cascade", + "aurora", + "serenity", + "echo", + "serenade", + "pinnacle", + "symphony", + "harmony", + "quasar", + "horizon", + "enigma", + "mirage", + "nebula", + "solitude", + "radiance", + "odyssey", + "zenith", + "aria", + "melody", + "celestia", + "seraph", + "infinity", + "eclipse", + "tornado", + "aurora", + "paradigm", + "solstice", + "phoenix", + "voyager", + "starlight", + "dreamer", + "cascade", + "nebula", + "serenade", + "pinnacle", + "symphony", + "harmony", + "zenith", + "mirage", + "eclipse", + "quasar", + "radiance", + "serenity", + "aurora", + "tornado", + "horizon" + ], + "maxSuffix": 10000 +} \ No newline at end of file diff --git a/lib/validate.js b/lib/validate.js index db5e8c28e..9a176d35f 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -225,6 +225,7 @@ export const settingsSchema = object({ ({ max, value }) => `${Math.abs(max - value.length)} too many`), hideBookmarks: boolean(), hideWalletBalance: boolean(), + diagnostics: boolean(), hideIsContributor: boolean() }) diff --git a/package-lock.json b/package-lock.json index 466b16bea..9196a0cf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@noble/curves": "^1.1.0", "@opensearch-project/opensearch": "^2.3.1", "@prisma/client": "^5.1.1", + "@slack/web-api": "^6.9.0", "acorn": "^8.10.0", "ajv": "^8.12.0", "async-retry": "^1.3.1", @@ -3071,6 +3072,54 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@slack/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "dependencies": { + "@types/node": ">=12.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/types": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.8.0.tgz", + "integrity": "sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-6.9.0.tgz", + "integrity": "sha512-RME5/F+jvQmZHkoP+ogrDbixq1Ms1mBmylzuWq4sf3f7GCpMPWoiZ+WqWk+sism3vrlveKWIgO9R4Qg9fiRyoQ==", + "dependencies": { + "@slack/logger": "^3.0.0", + "@slack/types": "^2.8.0", + "@types/is-stream": "^1.1.0", + "@types/node": ">=12.0.0", + "axios": "^0.27.2", + "eventemitter3": "^3.1.0", + "form-data": "^2.5.0", + "is-electron": "2.2.2", + "is-stream": "^1.1.0", + "p-queue": "^6.6.1", + "p-retry": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api/node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3312,6 +3361,14 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "node_modules/@types/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -3446,6 +3503,11 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -4073,6 +4135,28 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-inline-react-svg": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-2.0.2.tgz", @@ -7121,6 +7205,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8268,6 +8371,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13116,6 +13224,14 @@ "node": ">=8" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13175,6 +13291,44 @@ "url": "https://github.com/sindresorhus/p-memoize?sponsor=1" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -20218,6 +20372,44 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, + "@slack/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "requires": { + "@types/node": ">=12.0.0" + } + }, + "@slack/types": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.8.0.tgz", + "integrity": "sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==" + }, + "@slack/web-api": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-6.9.0.tgz", + "integrity": "sha512-RME5/F+jvQmZHkoP+ogrDbixq1Ms1mBmylzuWq4sf3f7GCpMPWoiZ+WqWk+sism3vrlveKWIgO9R4Qg9fiRyoQ==", + "requires": { + "@slack/logger": "^3.0.0", + "@slack/types": "^2.8.0", + "@types/is-stream": "^1.1.0", + "@types/node": ">=12.0.0", + "axios": "^0.27.2", + "eventemitter3": "^3.1.0", + "form-data": "^2.5.0", + "is-electron": "2.2.2", + "is-stream": "^1.1.0", + "p-queue": "^6.6.1", + "p-retry": "^4.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + } + } + }, "@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -20449,6 +20641,14 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "@types/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", + "requires": { + "@types/node": "*" + } + }, "@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -20582,6 +20782,11 @@ "@types/node": "*" } }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -21109,6 +21314,27 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "babel-plugin-inline-react-svg": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-2.0.2.tgz", @@ -23331,6 +23557,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -24142,6 +24373,11 @@ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" }, + "is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -27140,6 +27376,11 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -27175,6 +27416,32 @@ "type-fest": "^3.0.0" } }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", diff --git a/package.json b/package.json index b65ca8cb7..529a2b010 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@noble/curves": "^1.1.0", "@opensearch-project/opensearch": "^2.3.1", "@prisma/client": "^5.1.1", + "@slack/web-api": "^6.9.0", "acorn": "^8.10.0", "ajv": "^8.12.0", "async-retry": "^1.3.1", diff --git a/pages/_app.js b/pages/_app.js index 3f1bec753..7d15c2ac0 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -16,6 +16,7 @@ import { ServiceWorkerProvider } from '../components/serviceworker' import { SSR } from '../lib/constants' import NProgress from 'nprogress' import 'nprogress/nprogress.css' +import { LoggerProvider } from '../components/logger' NProgress.configure({ showSpinner: false @@ -88,19 +89,21 @@ function MyApp ({ Component, pageProps: { ...props } }) { - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/pages/api/log/index.js b/pages/api/log/index.js new file mode 100644 index 000000000..25150b917 --- /dev/null +++ b/pages/api/log/index.js @@ -0,0 +1,26 @@ +import models from '../../../api/models' +import slackClient from '../../../api/slack' + +const channelId = process.env.SLACK_CHANNEL_ID + +const toKV = (obj) => { + return obj ? Object.entries(obj).reduce((text, [k, v]) => text + ` ${k}=${v}`, '').trimStart() : '-' +} + +const slackPostMessage = ({ id, level, name, message, env, context }) => { + if (!slackClient) return + const text = `\`${new Date().toISOString()}\` | \`${id} [${level}] ${name}\` | ${message} | ${toKV(context)} | ${toKV({ os: env.os })}` + return slackClient.chat.postMessage({ channel: channelId, text }) +} + +export default async (req, res) => { + const { level, name, message, env, context } = req.body + if (!name) return res.status(400).json({ status: 400, message: 'name required' }) + if (!message) return res.status(400).json({ status: 400, message: 'message required' }) + + const { id } = await models.log.create({ data: { level: level.toUpperCase(), name, message, env, context } }) + + slackPostMessage({ id, ...req.body }).catch(console.error) + + return res.status(200).json({ status: 200, message: 'ok' }) +} diff --git a/pages/settings.js b/pages/settings.js index 872061982..09337c07d 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -23,6 +23,7 @@ import { useShowModal } from '../components/modal' import { authErrorMessage } from '../components/login' import { NostrAuth } from '../components/nostr-auth' import { useToast } from '../components/toast' +import { useLogger } from '../components/logger' import { useMe } from '../components/me' export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true }) @@ -47,6 +48,7 @@ export default function Settings ({ ssrData }) { } } ) + const logger = useLogger() const { data } = useQuery(SETTINGS) const { settings } = data || ssrData @@ -80,6 +82,7 @@ export default function Settings ({ ssrData }) { nostrRelays: settings?.nostrRelays?.length ? settings?.nostrRelays : [''], hideBookmarks: settings?.hideBookmarks, hideWalletBalance: settings?.hideWalletBalance, + diagnostics: settings?.diagnostics, hideIsContributor: settings?.hideIsContributor }} schema={settingsSchema} @@ -253,6 +256,29 @@ export default function Settings ({ ssrData }) { hide my bookmarks from other stackers} name='hideBookmarks' + groupClassName='mb-0' + /> + allow diagnostics + +
    +
  • collect and send back diagnostics data
  • +
  • this information is used to identify and fix bugs
  • +
  • this information includes: +
    • timestamps
    +
    • a randomly generated fancy name
    +
    • your user agent
    +
    • your operating system
    +
  • +
  • this information can not be traced back to you without your fancy name
  • +
  • fancy names are generated in your browser
  • +
+
your fancy name: {logger.name}
+
+ + } + name='diagnostics' />
content
1) { - console.error(`more than one notification with tag ${tag} found`) + const message = `[sw:push] more than one notification with tag ${tag} found` + messageChannelPort?.postMessage({ level: 'error', message }) + console.error(message) return null } if (notifications.length === 0) { @@ -85,7 +88,12 @@ self.addEventListener('notificationclick', (event) => { // https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f self.addEventListener('message', (event) => { + if (event.data.action === 'MESSAGE_PORT') { + messageChannelPort = event.ports[0] + } + messageChannelPort?.postMessage({ message: '[sw:message] received message', context: { action: event.data.action } }) if (event.data.action === 'STORE_SUBSCRIPTION') { + messageChannelPort?.postMessage({ message: '[sw:message] storing subscription in IndexedDB', context: { endpoint: event.data.subscription.endpoint } }) return event.waitUntil(storage.setItem('subscription', event.data.subscription)) } if (event.data.action === 'SYNC_SUBSCRIPTION') { @@ -96,14 +104,17 @@ self.addEventListener('message', (event) => { async function handlePushSubscriptionChange (oldSubscription, newSubscription) { // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event // fallbacks since browser may not set oldSubscription and newSubscription + messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] invoked' }) oldSubscription ??= await storage.getItem('subscription') newSubscription ??= await self.registration.pushManager.getSubscription() if (!newSubscription) { // no subscription exists at the moment + messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] no existing subscription found' }) return } if (oldSubscription?.endpoint === newSubscription.endpoint) { // subscription did not change. no need to sync with server + messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] old subscription matches existing subscription' }) return } // convert keys from ArrayBuffer to string @@ -128,9 +139,11 @@ async function handlePushSubscriptionChange (oldSubscription, newSubscription) { }, body }) + messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] synced push subscription with server', context: { endpoint: variables.endpoint, oldEndpoint: variables.oldEndpoint } }) await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription))) } self.addEventListener('pushsubscriptionchange', (event) => { + messageChannelPort?.postMessage({ message: '[sw:pushsubscriptionchange] received event' }) event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription)) }, false)