Add setting to send diagnostics back to SN (#463)

* Add diagnostics settings & endpoint

Stackers can now help us to identify and fix bugs by enabling diagnostics.

This will send anonymized data to us.

For now, this is only used to send events around push notifications.

* Send diagnostics to slack

* Detect OS

* Diagnostics data is only pseudonymous, not anonymous

It's only pseudonymous since with additional knowledge (which stacker uses which fancy name), we could trace the events back to individual stackers.

Data is only anonymous if this is not possible - it must be irreversible.

* Check if window.navigator is defined

* Use Slack SDK

* Catch errors of slack requests

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
ekzyis 2023-09-19 01:00:16 +02:00 committed by GitHub
parent e5852ee0b1
commit 3a7c3f7af2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 754 additions and 21 deletions

View File

@ -66,3 +66,7 @@ DATABASE_URL="postgresql://sn:password@db:5432/stackernews?schema=public"
POSTGRES_PASSWORD=password POSTGRES_PASSWORD=password
POSTGRES_USER=sn POSTGRES_USER=sn
POSTGRES_DB=stackernews POSTGRES_DB=stackernews
# slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=

17
api/slack/index.js Normal file
View File

@ -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

View File

@ -25,7 +25,7 @@ export default gql`
noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!,
hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, clickToLoadImg: Boolean!, hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, clickToLoadImg: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!], hideBookmarks: 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! setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User! upsertBio(bio: String!): User!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
@ -87,6 +87,7 @@ export default gql`
hideBookmarks: Boolean! hideBookmarks: Boolean!
hideWelcomeBanner: Boolean! hideWelcomeBanner: Boolean!
hideWalletBalance: Boolean! hideWalletBalance: Boolean!
diagnostics: Boolean!
clickToLoadImg: Boolean! clickToLoadImg: Boolean!
wildWestMode: Boolean! wildWestMode: Boolean!
greeterMode: Boolean! greeterMode: Boolean!

109
components/logger.js Normal file
View File

@ -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 (
<LoggerContext.Provider value={logger}>
{children}
</LoggerContext.Provider>
)
}
export function useLogger () {
return useContext(LoggerContext)
}

View File

@ -1,6 +1,7 @@
import { createContext, useContext, useEffect, useState, useCallback } from 'react' import { createContext, useContext, useEffect, useState, useCallback } from 'react'
import { Workbox } from 'workbox-window' import { Workbox } from 'workbox-window'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { useLogger } from './logger'
const applicationServerKey = process.env.NEXT_PUBLIC_VAPID_PUBKEY 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, // I am not entirely sure if this is needed since at least in Brave,
// using `registration.pushManager.subscribe` also prompts the user. // 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 // Brave users must enable a flag in brave://settings/privacy first
// see https://stackoverflow.com/a/69624651 // see https://stackoverflow.com/a/69624651
let pushSubscription = await registration.pushManager.subscribe(subscribeOptions) let pushSubscription = await registration.pushManager.subscribe(subscribeOptions)
const { endpoint } = pushSubscription
logger.info('subscribed to push notifications', { endpoint })
// convert keys from ArrayBuffer to string // convert keys from ArrayBuffer to string
pushSubscription = JSON.parse(JSON.stringify(pushSubscription)) pushSubscription = JSON.parse(JSON.stringify(pushSubscription))
// Send subscription to service worker to save it so we can use it later during `pushsubscriptionchange` // 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', action: 'STORE_SUBSCRIPTION',
subscription: pushSubscription subscription: pushSubscription
}) })
logger.info('sent STORE_SUBSCRIPTION to service worker', { endpoint })
// send subscription to server // send subscription to server
const variables = { const variables = {
endpoint: pushSubscription.endpoint, endpoint,
p256dh: pushSubscription.keys.p256dh, p256dh: pushSubscription.keys.p256dh,
auth: pushSubscription.keys.auth auth: pushSubscription.keys.auth
} }
await savePushSubscription({ variables }) await savePushSubscription({ variables })
logger.info('sent push subscription to server', { endpoint })
} }
const unsubscribeFromPushNotifications = async (subscription) => { const unsubscribeFromPushNotifications = async (subscription) => {
await subscription.unsubscribe() await subscription.unsubscribe()
const { endpoint } = subscription const { endpoint } = subscription
logger.info('unsubscribed from push notifications', { endpoint })
await deletePushSubscription({ variables: { endpoint } }) await deletePushSubscription({ variables: { endpoint } })
logger.info('deleted push subscription from server', { endpoint })
} }
const togglePushSubscription = useCallback(async () => { const togglePushSubscription = useCallback(async () => {
const pushSubscription = await registration.pushManager.getSubscription() const pushSubscription = await registration.pushManager.getSubscription()
if (pushSubscription) return unsubscribeFromPushNotifications(pushSubscription) if (pushSubscription) {
return unsubscribeFromPushNotifications(pushSubscription)
}
return subscribeToPushNotifications() 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. // 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 // see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
navigator?.serviceWorker?.controller?.postMessage?.({ action: 'SYNC_SUBSCRIPTION' }) navigator?.serviceWorker?.controller?.postMessage?.({ action: 'SYNC_SUBSCRIPTION' })
logger.info('sent SYNC_SUBSCRIPTION to service worker')
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!support.serviceWorker) return if (!support.serviceWorker) {
logger.info('device does not support service worker')
return
}
const wb = new Workbox('/sw.js', { scope: '/' }) const wb = new Workbox('/sw.js', { scope: '/' })
wb.register().then(registration => { wb.register().then(registration => {
logger.info('service worker registration successful')
setRegistration(registration) setRegistration(registration)
}) })
}, [support.serviceWorker]) }, [support.serviceWorker])

View File

@ -31,6 +31,7 @@ export const ME = gql`
hideFromTopUsers hideFromTopUsers
hideCowboyHat hideCowboyHat
clickToLoadImg clickToLoadImg
diagnostics
wildWestMode wildWestMode
greeterMode greeterMode
lastCheckedJobs lastCheckedJobs
@ -62,6 +63,7 @@ export const SETTINGS_FIELDS = gql`
hideIsContributor hideIsContributor
clickToLoadImg clickToLoadImg
hideWalletBalance hideWalletBalance
diagnostics
nostrPubkey nostrPubkey
nostrRelays nostrRelays
wildWestMode wildWestMode
@ -91,14 +93,14 @@ mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency:
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!,
$hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $clickToLoadImg: Boolean!, $hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $clickToLoadImg: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!], $hideBookmarks: 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, setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency,
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc,
hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, clickToLoadImg: $clickToLoadImg, hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, clickToLoadImg: $clickToLoadImg,
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays, hideBookmarks: $hideBookmarks, 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 ...SettingsFields
} }
} }

207
lib/fancy-names.json Normal file
View File

@ -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
}

View File

@ -225,6 +225,7 @@ export const settingsSchema = object({
({ max, value }) => `${Math.abs(max - value.length)} too many`), ({ max, value }) => `${Math.abs(max - value.length)} too many`),
hideBookmarks: boolean(), hideBookmarks: boolean(),
hideWalletBalance: boolean(), hideWalletBalance: boolean(),
diagnostics: boolean(),
hideIsContributor: boolean() hideIsContributor: boolean()
}) })

267
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@noble/curves": "^1.1.0", "@noble/curves": "^1.1.0",
"@opensearch-project/opensearch": "^2.3.1", "@opensearch-project/opensearch": "^2.3.1",
"@prisma/client": "^5.1.1", "@prisma/client": "^5.1.1",
"@slack/web-api": "^6.9.0",
"acorn": "^8.10.0", "acorn": "^8.10.0",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"async-retry": "^1.3.1", "async-retry": "^1.3.1",
@ -3071,6 +3072,54 @@
"url": "https://github.com/sindresorhus/is?sponsor=1" "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": { "node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "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", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" "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": { "node_modules/@types/json-schema": {
"version": "7.0.12", "version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@ -3446,6 +3503,11 @@
"@types/node": "*" "@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": { "node_modules/@types/scheduler": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "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", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" "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": { "node_modules/babel-plugin-inline-react-svg": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-2.0.2.tgz", "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==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "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": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -8268,6 +8371,11 @@
"url": "https://github.com/sponsors/wooorm" "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": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -13116,6 +13224,14 @@
"node": ">=8" "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": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "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" "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": { "node_modules/p-try": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" "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": { "@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "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", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" "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": { "@types/json-schema": {
"version": "7.0.12", "version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@ -20582,6 +20782,11 @@
"@types/node": "*" "@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": { "@types/scheduler": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "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", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" "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": { "babel-plugin-inline-react-svg": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-inline-react-svg/-/babel-plugin-inline-react-svg-2.0.2.tgz", "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==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "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": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "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", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" "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": { "is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" "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": { "p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -27175,6 +27416,32 @@
"type-fest": "^3.0.0" "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": { "p-try": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",

View File

@ -18,6 +18,7 @@
"@noble/curves": "^1.1.0", "@noble/curves": "^1.1.0",
"@opensearch-project/opensearch": "^2.3.1", "@opensearch-project/opensearch": "^2.3.1",
"@prisma/client": "^5.1.1", "@prisma/client": "^5.1.1",
"@slack/web-api": "^6.9.0",
"acorn": "^8.10.0", "acorn": "^8.10.0",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"async-retry": "^1.3.1", "async-retry": "^1.3.1",

View File

@ -16,6 +16,7 @@ import { ServiceWorkerProvider } from '../components/serviceworker'
import { SSR } from '../lib/constants' import { SSR } from '../lib/constants'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import { LoggerProvider } from '../components/logger'
NProgress.configure({ NProgress.configure({
showSpinner: false showSpinner: false
@ -88,6 +89,7 @@ function MyApp ({ Component, pageProps: { ...props } }) {
<PlausibleProvider domain='stacker.news' trackOutboundLinks> <PlausibleProvider domain='stacker.news' trackOutboundLinks>
<ApolloProvider client={client}> <ApolloProvider client={client}>
<MeProvider me={me}> <MeProvider me={me}>
<LoggerProvider>
<ServiceWorkerProvider> <ServiceWorkerProvider>
<PriceProvider price={price}> <PriceProvider price={price}>
<LightningProvider> <LightningProvider>
@ -101,6 +103,7 @@ function MyApp ({ Component, pageProps: { ...props } }) {
</LightningProvider> </LightningProvider>
</PriceProvider> </PriceProvider>
</ServiceWorkerProvider> </ServiceWorkerProvider>
</LoggerProvider>
</MeProvider> </MeProvider>
</ApolloProvider> </ApolloProvider>
</PlausibleProvider> </PlausibleProvider>

26
pages/api/log/index.js Normal file
View File

@ -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' })
}

View File

@ -23,6 +23,7 @@ import { useShowModal } from '../components/modal'
import { authErrorMessage } from '../components/login' import { authErrorMessage } from '../components/login'
import { NostrAuth } from '../components/nostr-auth' import { NostrAuth } from '../components/nostr-auth'
import { useToast } from '../components/toast' import { useToast } from '../components/toast'
import { useLogger } from '../components/logger'
import { useMe } from '../components/me' import { useMe } from '../components/me'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true }) 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 { data } = useQuery(SETTINGS)
const { settings } = data || ssrData const { settings } = data || ssrData
@ -80,6 +82,7 @@ export default function Settings ({ ssrData }) {
nostrRelays: settings?.nostrRelays?.length ? settings?.nostrRelays : [''], nostrRelays: settings?.nostrRelays?.length ? settings?.nostrRelays : [''],
hideBookmarks: settings?.hideBookmarks, hideBookmarks: settings?.hideBookmarks,
hideWalletBalance: settings?.hideWalletBalance, hideWalletBalance: settings?.hideWalletBalance,
diagnostics: settings?.diagnostics,
hideIsContributor: settings?.hideIsContributor hideIsContributor: settings?.hideIsContributor
}} }}
schema={settingsSchema} schema={settingsSchema}
@ -253,6 +256,29 @@ export default function Settings ({ ssrData }) {
<Checkbox <Checkbox
label={<>hide my bookmarks from other stackers</>} label={<>hide my bookmarks from other stackers</>}
name='hideBookmarks' name='hideBookmarks'
groupClassName='mb-0'
/>
<Checkbox
label={
<div className='d-flex align-items-center'>allow diagnostics
<Info>
<ul className='fw-bold'>
<li>collect and send back diagnostics data</li>
<li>this information is used to identify and fix bugs</li>
<li>this information includes:
<ul><li>timestamps</li></ul>
<ul><li>a randomly generated fancy name</li></ul>
<ul><li>your user agent</li></ul>
<ul><li>your operating system</li></ul>
</li>
<li>this information can not be traced back to you without your fancy name</li>
<li>fancy names are generated in your browser</li>
</ul>
<div className='text-muted fst-italic'>your fancy name: {logger.name}</div>
</Info>
</div>
}
name='diagnostics'
/> />
<div className='form-label'>content</div> <div className='form-label'>content</div>
<Checkbox <Checkbox

View File

@ -0,0 +1,21 @@
-- CreateEnum
CREATE TYPE "LogLevel" AS ENUM ('DEBUG', 'INFO', 'WARN', 'ERROR');
-- AlterTable
ALTER TABLE "users" ADD COLUMN "diagnostics" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "Log" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"level" "LogLevel" NOT NULL,
"name" TEXT NOT NULL,
"message" TEXT NOT NULL,
"env" JSONB,
"context" JSONB,
CONSTRAINT "Log_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Log.name_index" ON "Log"("created_at", "name");

View File

@ -90,6 +90,7 @@ model User {
followers UserSubscription[] @relation("follower") followers UserSubscription[] @relation("follower")
followees UserSubscription[] @relation("followee") followees UserSubscription[] @relation("followee")
hideWelcomeBanner Boolean @default(false) hideWelcomeBanner Boolean @default(false)
diagnostics Boolean @default(false)
hideIsContributor Boolean @default(false) hideIsContributor Boolean @default(false)
@@index([createdAt], map: "users.created_at_index") @@index([createdAt], map: "users.created_at_index")
@ -561,6 +562,18 @@ model PushSubscription {
@@index([userId], map: "PushSubscription.userId_index") @@index([userId], map: "PushSubscription.userId_index")
} }
model Log {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
level LogLevel
name String
message String
env Json?
context Json?
@@index([createdAt, name], map: "Log.name_index")
}
enum EarnType { enum EarnType {
POST POST
COMMENT COMMENT
@ -606,3 +619,10 @@ enum WithdrawlStatus {
CONFIRMED CONFIRMED
UNKNOWN_FAILURE UNKNOWN_FAILURE
} }
enum LogLevel {
DEBUG
INFO
WARN
ERROR
}

View File

@ -11,6 +11,7 @@ import ServiceWorkerStorage from 'serviceworker-storage'
self.__WB_DISABLE_DEV_LOGS = true self.__WB_DISABLE_DEV_LOGS = true
const storage = new ServiceWorkerStorage('sw:storage', 1) const storage = new ServiceWorkerStorage('sw:storage', 1)
let messageChannelPort
// preloading improves startup performance // preloading improves startup performance
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/ // https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
@ -55,7 +56,9 @@ self.addEventListener('push', async function (event) {
const notifications = await self.registration.getNotifications({ tag }) const notifications = await self.registration.getNotifications({ tag })
// since we used a tag filter, there should only be zero or one notification // since we used a tag filter, there should only be zero or one notification
if (notifications.length > 1) { if (notifications.length > 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 return null
} }
if (notifications.length === 0) { 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 // https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
self.addEventListener('message', (event) => { 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') { 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)) return event.waitUntil(storage.setItem('subscription', event.data.subscription))
} }
if (event.data.action === 'SYNC_SUBSCRIPTION') { if (event.data.action === 'SYNC_SUBSCRIPTION') {
@ -96,14 +104,17 @@ self.addEventListener('message', (event) => {
async function handlePushSubscriptionChange (oldSubscription, newSubscription) { async function handlePushSubscriptionChange (oldSubscription, newSubscription) {
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event
// fallbacks since browser may not set oldSubscription and newSubscription // fallbacks since browser may not set oldSubscription and newSubscription
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] invoked' })
oldSubscription ??= await storage.getItem('subscription') oldSubscription ??= await storage.getItem('subscription')
newSubscription ??= await self.registration.pushManager.getSubscription() newSubscription ??= await self.registration.pushManager.getSubscription()
if (!newSubscription) { if (!newSubscription) {
// no subscription exists at the moment // no subscription exists at the moment
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] no existing subscription found' })
return return
} }
if (oldSubscription?.endpoint === newSubscription.endpoint) { if (oldSubscription?.endpoint === newSubscription.endpoint) {
// subscription did not change. no need to sync with server // subscription did not change. no need to sync with server
messageChannelPort?.postMessage({ message: '[sw:handlePushSubscriptionChange] old subscription matches existing subscription' })
return return
} }
// convert keys from ArrayBuffer to string // convert keys from ArrayBuffer to string
@ -128,9 +139,11 @@ async function handlePushSubscriptionChange (oldSubscription, newSubscription) {
}, },
body 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))) await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription)))
} }
self.addEventListener('pushsubscriptionchange', (event) => { self.addEventListener('pushsubscriptionchange', (event) => {
messageChannelPort?.postMessage({ message: '[sw:pushsubscriptionchange] received event' })
event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription)) event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription))
}, false) }, false)