Skip to content

Commit

Permalink
SW logs
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Oct 19, 2024
1 parent c8a4441 commit 17f3e77
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 42 deletions.
8 changes: 4 additions & 4 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ sbp('okTurtles.events/on', MESSAGE_RECEIVE_RAW, ({
newMessage
}) => {
const state = sbp('chelonia/contract/state', contractID)
const getters = sbp('state/vuex/getters')
const mentions = makeMentionFromUserID(getters.ourIdentityContractId)
const rootState = sbp('chelonia/rootState')
const mentions = makeMentionFromUserID(rootState.loggedIn?.identityContractID)
const msgData = newMessage || data
const isMentionedMe = (!!newMessage || data.type === MESSAGE_TYPES.TEXT) && msgData.text &&
(msgData.text.includes(mentions.me) || msgData.text.includes(mentions.all))

if (!newMessage) {
const isAlreadyAdded = !!getters
.chatRoomUnreadMessages(contractID).find(m => m.messageHash === data.hash)
// TODO: rootState.unreadMessages for SW
const isAlreadyAdded = rootState.unreadMessages?.[contractID]?.unreadMessages.find(m => m.messageHash === data.hash)

if (isAlreadyAdded && !isMentionedMe) {
sbp('gi.actions/identity/kv/removeChatRoomUnreadMessage', { contractID, messageHash: data.hash })
Expand Down
9 changes: 9 additions & 0 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ export default (sbp('sbp/selectors/register', {
console.debug('[gi.actions/identity/login] Scheduled call starting', identityContractID)
transientSecretKeys = transientSecretKeys.map(k => ({ key: deserializeKey(k.valueOf()), transient: true }))

// If running in a SW, start log capture here
if (typeof WorkerGlobalScope === 'function') {
await sbp('swLogs/startCapture', identityContractID)
}
await sbp('chelonia/reset', { ...cheloniaState, loggedIn: { identityContractID } })
await sbp('chelonia/storeSecretKeys', new Secret(transientSecretKeys))

Expand Down Expand Up @@ -404,6 +408,11 @@ export default (sbp('sbp/selectors/register', {
}
// Clear the file cache when logging out to preserve privacy
sbp('gi.db/filesCache/clear').catch((e) => { console.error('Error clearing file cache', e) })
// If running inside a SW, clear logs
if (typeof WorkerGlobalScope === 'function') {
// clear stored logs to prevent someone else accessing sensitve data
sbp('swLogs/pauseCapture', { wipeOut: true }).catch((e) => { console.error('Error clearing file cache', e) })
}
sbp('okTurtles.events/emit', LOGOUT)
return cheloniaState
},
Expand Down
11 changes: 9 additions & 2 deletions frontend/controller/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { PUBSUB_INSTANCE } from '@controller/instance-keys.js'
import sbp from '@sbp/sbp'
import { PWA_INSTALLABLE } from '@utils/events.js'
import { LOGIN_COMPLETE, PWA_INSTALLABLE, SET_APP_LOGS_FILTER } from '@utils/events.js'
import { HOURS_MILLIS } from '~/frontend/model/contracts/shared/time.js'
import { PUBSUB_RECONNECTION_SUCCEEDED, PUSH_SERVER_ACTION_TYPE, REQUEST_TYPE, createMessage } from '~/shared/pubsub.js'
import { deserializer } from '~/shared/serdes/index.js'
Expand Down Expand Up @@ -253,7 +253,14 @@ sbp('sbp/selectors/register', {
const result = await pwa.deferredInstallPrompt.prompt()
return result.outcome
}
})
});

// Events that need to be relayed to the SW
[LOGIN_COMPLETE, SET_APP_LOGS_FILTER].forEach((event) =>
sbp('okTurtles.events/on', event, (...data) => {
navigator.serviceWorker.controller?.postMessage({ type: 'event', subtype: event, data })
})
)

// helper method

Expand Down
41 changes: 32 additions & 9 deletions frontend/controller/serviceworkers/sw-primary.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

import { PROPOSAL_ARCHIVED } from '@model/contracts/shared/constants.js'
import '@model/swCaptureLogs.js'
import '@sbp/okturtles.data'
import '@sbp/okturtles.eventqueue'
import '@sbp/okturtles.events'
Expand All @@ -27,8 +28,28 @@ deserializer.register(Secret)
// https://frontendian.co/service-workers
// https://stackoverflow.com/a/49748437 => https://medium.com/@nekrtemplar/self-destroying-serviceworker-73d62921d717 => https://love2dev.com/blog/how-to-uninstall-a-service-worker/

const reducer = (o, v) => { o[v] = true; return o }
// Domains for which debug logging won't be enabled.
const domainBlacklist = [
'sbp',
'okTurtles.data'
].reduce(reducer, {})
// Selectors for which debug logging won't be enabled.
const selectorBlacklist = [
'chelonia/db/get',
'chelonia/db/set',
'chelonia/rootState',
'chelonia/haveSecretKey',
'chelonia/private/enqueuePostSyncOps',
'chelonia/private/invoke',
'state/vuex/state',
'state/vuex/getters',
'state/vuex/settings',
'gi.db/settings/save',
'gi.db/logs/save'
].reduce(reducer, {})
sbp('sbp/filters/global/add', (domain, selector, data) => {
// if (domainBlacklist[domain] || selectorBlacklist[selector]) return
if (domainBlacklist[domain] || selectorBlacklist[selector]) return
console.debug(`[sw] [sbp] ${selector}`, data)
});

Expand All @@ -50,10 +71,7 @@ sbp('sbp/filters/global/add', (domain, selector, data) => {
})

sbp('sbp/selectors/register', {
'state/vuex/state': () => {
// TODO: Remove this selector once it's removed from contracts
return sbp('chelonia/rootState')
},
'state/vuex/state': () => sbp('chelonia/rootState'),
'state/vuex/getters': () => {
const obj = Object.create(null)
Object.defineProperties(obj, Object.fromEntries(Object.entries(getters).map(([getter, fn]: [string, Function]) => {
Expand Down Expand Up @@ -83,11 +101,11 @@ sbp('sbp/selectors/register', {
})

sbp('sbp/selectors/register', {
// TODO: Implement this (and some other logs-related selectors, such as for
// starting capture, pausing capture)
'appLogs/save': () => Promise.resolve(undefined)
'appLogs/save': () => sbp('swLogs/save')
})

const setupPromise = setupChelonia()

self.addEventListener('install', function (event) {
console.debug('[sw] install')
event.waitUntil(self.skipWaiting())
Expand All @@ -97,7 +115,7 @@ self.addEventListener('activate', function (event) {
console.debug('[sw] activate')

// 'clients.claim()' reference: https://web.dev/articles/service-worker-lifecycle#clientsclaim
event.waitUntil(setupChelonia().then(() => self.clients.claim()))
event.waitUntil(setupPromise.then(() => self.clients.claim()))
})

self.addEventListener('fetch', function (event) {
Expand Down Expand Up @@ -163,6 +181,11 @@ self.addEventListener('message', function (event) {
clients.forEach(client => client.navigate(client.url))
})
break
case 'event':
console.error('@@@SW EVENT RECEIVED', event.data.subtype, ...deserializer(event.data.data))
// TODO: UNCOMMENT
// // sbp('okTurtles.events/emit', event.data.subtype, ...deserializer(event.data.data))
break
default:
console.error('[sw] unknown message type:', event.data)
break
Expand Down
4 changes: 4 additions & 0 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,16 @@ async function startApp () {
})
sbp('sbp/selectors/register', {
'sw-namespace/*': (...args) => {
// Remove the `sw-` prefix from the selector
return swRpc(args[0].slice(3), ...args.slice(1))
}
})
sbp('sbp/selectors/register', {
'gi.notifications/*': swRpc
})
sbp('sbp/selectors/register', {
'swLogs/*': swRpc
})

/* eslint-disable no-new */
new Vue({
Expand Down
35 changes: 13 additions & 22 deletions frontend/model/captureLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SET_APP_LOGS_FILTER } from '~/frontend/utils/events.js'
import { MAX_LOG_ENTRIES } from '~/frontend/utils/constants.js'
import { L } from '@common/common.js'
import { createLogger } from './logger.js'
import logServer from './logServer.js'

/*
- giConsole/[username]/entries - the stored log entries.
Expand All @@ -18,7 +19,7 @@ const originalConsole = self.console
let logger: Object = null
let identityContractID: string = ''

// A default storage backend using `localStorage`.
// A default storage backend using `sessionStorage`.
const getItem = (key: string): ?string => sessionStorage.getItem(`giConsole/${identityContractID}/${key}`)
const removeItem = (key: string): void => sessionStorage.removeItem(`giConsole/${identityContractID}/${key}`)
const setItem = (key: string, value: any): void => {
Expand Down Expand Up @@ -46,7 +47,7 @@ async function captureLogsStart (userLogged: string) {
// NEW_VISIT -> The user comes from an ongoing session (refresh or login).
const isNewSession = !sessionStorage.getItem('NEW_SESSION')
if (isNewSession) { sessionStorage.setItem('NEW_SESSION', '1') }
console.log(isNewSession ? 'NEW_SESSION' : 'NEW_VISIT', 'Starting to capture logs of type:', logger.appLogsFilter)
originalConsole.log(isNewSession ? 'NEW_SESSION' : 'NEW_VISIT', 'Starting to capture logs of type:', logger.appLogsFilter)
}

async function captureLogsPause ({ wipeOut }: { wipeOut: boolean }): Promise<void> {
Expand Down Expand Up @@ -92,11 +93,18 @@ function downloadOrShareLogs (actionType: 'share' | 'download', elLink?: HTMLAnc
}
}

// The reason to use the 'visibilitychange' event over the 'beforeunload' event
// is that the latter is unreliable on mobile. For example, if a tab is set to
// the background and then closed, the 'beforeunload' event may never be fired.
// Furthermore, 'beforeunload' has implications for how the bfcache works.
window.addEventListener('visibilitychange', event => sbp('appLogs/save').catch(e => {
console.error('Error saving logs during visibilitychange event handler', e)
}))

sbp('sbp/selectors/register', {
// Enable logging to the server
logServer(originalConsole)

export default (sbp('sbp/selectors/register', {
'appLogs/downloadOrShare': downloadOrShareLogs,
'appLogs/get' () { return logger?.entries.toArray() ?? [] },
async 'appLogs/save' () { await logger?.save() },
Expand All @@ -107,22 +115,5 @@ sbp('sbp/selectors/register', {
identityContractID = userID
try { await clearLogs() } catch {}
identityContractID = savedID
},
// only log to server if we're in development mode and connected over the tunnel (which creates URLs that
// begin with 'https://gi' per Gruntfile.js)
'appLogs/logServer': process.env.NODE_ENV !== 'development' || !window.location.href.startsWith('https://gi')
? () => {}
: function (level, stringifyMe) {
if (level === 'debug') return // comment out to send much more log info
const value = JSON.stringify(stringifyMe)
fetch(`${sbp('okTurtles.data/get', 'API_URL')}/log`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ level, value })
}).catch(e => {
originalConsole.error(`[captureLogs] '${e.message}' attempting to log [${level}] to server:`, value)
})
}
})
}
}): string[])
26 changes: 25 additions & 1 deletion frontend/model/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ sbp('sbp/selectors/register', {
})

// ======================================
// Archve for proposals and anything else
// Archive for proposals and anything else
// ======================================

const archive = localforage.createInstance({
Expand All @@ -382,3 +382,27 @@ sbp('sbp/selectors/register', {
return archive.clear()
}
})

// ======================================
// Application logs, used for the SW logs
// ======================================

const logs = localforage.createInstance({
name: 'Group Income',
storeName: 'Logs'
})

sbp('sbp/selectors/register', {
'gi.db/logs/save': function (key: string, value: any): Promise<*> {
return logs.setItem(key, value)
},
'gi.db/logs/load': function (key: string): Promise<any> {
return logs.getItem(key)
},
'gi.db/logs/delete': function (key: string): Promise<Object> {
return logs.removeItem(key)
},
'gi.db/logs/clear': function (): Promise<any> {
return logs.clear()
}
})
28 changes: 28 additions & 0 deletions frontend/model/logServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import sbp from '@sbp/sbp'
import '@sbp/okturtles.events'
import { CAPTURED_LOGS } from '~/frontend/utils/events.js'

export default (console: Object): Function => {
// only log to server if we're in development mode and connected over the
// tunnel (which creates URLs that begin with 'https://gi' per Gruntfile.js)
if (process.env.NODE_ENV !== 'development' || !self.location.href.startsWith('https://gi')) return

sbp('okTurtles.events/on', CAPTURED_LOGS, ({ level, msg: stringifyMe }) => {
if (level === 'debug') return // comment out to send much more log info
const value = JSON.stringify(stringifyMe)
// To avoid infinite loop because we log all selector calls, we run sbp calls
// here in a roundabout way by getting the function to which they're mapped.
// The reason this works is because the entire `sbp` domain is blacklisted
// from being logged.
const apiUrl = sbp('sbp/selectors/fn', 'okTurtles.data/get')('API_URL')
fetch(`${apiUrl}/log`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ level, value })
}).catch(e => {
console.error(`[captureLogs] '${e.message}' attempting to log [${level}] to server:`, value)
})
})
}
1 change: 0 additions & 1 deletion frontend/model/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,4 @@ function captureLogEntry (logger: Object, type: string, ...args) {
// The reason this works is because the entire `sbp` domain is blacklisted
// from being logged in main.js.
sbp('sbp/selectors/fn', 'okTurtles.events/emit')(CAPTURED_LOGS, entry)
sbp('sbp/selectors/fn', 'appLogs/logServer')(type, entry.msg)
}
2 changes: 1 addition & 1 deletion frontend/model/settings/vuexModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const defaultTheme = 'system'
const defaultColor: string = checkSystemColor()

export const defaultSettings = {
appLogsFilter: (((process.env.NODE_ENV === 'development' || new URLSearchParams(window.location.search).get('debug'))
appLogsFilter: (((process.env.NODE_ENV === 'development' || new URLSearchParams(location.search).get('debug'))
? ['error', 'warn', 'info', 'debug', 'log']
: ['error', 'warn', 'info']): string[]),
fontSize: 16,
Expand Down
Loading

0 comments on commit 17f3e77

Please sign in to comment.