Add log sending

This commit is contained in:
Jon Staab 2026-08-13 12:57:42 -07:00
parent 2d89881aac
commit db68aa4a68
8 changed files with 131 additions and 13 deletions

1
.env
View file

@ -9,6 +9,7 @@ VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
VITE_PLATFORM_NAME=Flotilla
VITE_PLATFORM_LOGO=static/logo.png
VITE_PLATFORM_RELAYS=
VITE_PLATFORM_LOGEE=be523f2d2255fa96b281233ba9df60d63ef74f874bea96651edd4e1f79785703
VITE_PLATFORM_ACCENT="#7161FF"
VITE_THEME="clay"
VITE_PLATFORM_DESCRIPTION="The chat app built for self-hosted communities."

View file

@ -16,6 +16,7 @@ You can also optionally create an `.env.local` file and populate it with the fol
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
- `VITE_PLATFORM_TERMS` - URL to your terms of service page
- `VITE_PLATFORM_PRIVACY` - URL to your privacy policy page
- `VITE_PLATFORM_LOGEE` - A hex pubkey which will receive logs users send from their privacy settings
**Platform mode**
- `VITE_PLATFORM_RELAYS` - A comma-separated list of relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.

View file

@ -12,6 +12,7 @@ import {
Domain,
FollowLists,
Handles,
Logger,
MessagingRelayLists,
MuteLists,
Network,
@ -142,6 +143,7 @@ export const deletes = usePlugin(Deletes)
export const domain = usePlugin(Domain)
export const followLists = usePlugin(FollowLists)
export const handles = usePlugin(Handles)
export const logger = usePlugin(Logger)
export const messagingRelayLists = usePlugin(MessagingRelayLists)
export const muteLists = usePlugin(MuteLists)
export const network = usePlugin(Network)

View file

@ -50,6 +50,8 @@ export const PLATFORM_LOGO = import.meta.env.PROD
export const PLATFORM_NAME = env("VITE_PLATFORM_NAME")
export const PLATFORM_LOGEE = env("VITE_PLATFORM_LOGEE")
export const PLATFORM_ACCENT = env("VITE_PLATFORM_ACCENT")
// components visual preset (see src/lib/components/theme.css). Selected per

86
src/app/logger.ts Normal file
View file

@ -0,0 +1,86 @@
import type {Unsubscriber} from "svelte/store"
import {Capacitor} from "@capacitor/core"
import {tryCatch} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {DirectMessage} from "@welshman/domain"
import type {Logger} from "@welshman/app"
import {logger, wraps, writer} from "@app/core"
import {PLATFORM_LOGEE} from "@app/env"
const formatValue = (value: unknown) => {
if (value instanceof Error) return value.stack || value.message
if (typeof value === "string") return value
return tryCatch(() => JSON.stringify(value)) || String(value)
}
const bindLogging = ($logger: Logger) => {
const {error} = console
const log = (source: string, value: unknown) => $logger.log(source, {message: formatValue(value)})
const onError = (event: ErrorEvent) => log("error", event.error || event.message)
const onUnhandledRejection = (event: PromiseRejectionEvent) => log("rejection", event.reason)
console.error = (...args: unknown[]) => {
log("console", args.map(formatValue).join(" "))
error(...args)
}
$logger.log("app", {
platform: Capacitor.getPlatform(),
build: import.meta.env.VITE_BUILD_HASH,
userAgent: navigator.userAgent,
})
window.addEventListener("error", onError)
window.addEventListener("unhandledrejection", onUnhandledRejection)
return () => {
console.error = error
window.removeEventListener("error", onError)
window.removeEventListener("unhandledrejection", onUnhandledRejection)
}
}
// Errors are only useful after the fact if someone kept them around, so collect them into the
// app's logger, which the user can hand over via `sendLogs` when something goes wrong. Logging in
// swaps in a fresh app with an empty log, so re-bind against the new one when that happens.
export const setupLogging = () => {
let unbind: Maybe<Unsubscriber>
const unsubscribe = logger.subscribe($logger => {
unbind?.()
unbind = bindLogging($logger)
})
return () => {
unbind?.()
unsubscribe()
}
}
export const sendLogs = async () => {
const messages = logger.get().messages.get()
const lines: string[] = []
let size = 0
// Relays commonly cap events at 64kb, so send only as much recent history as will fit
for (const message of messages.slice().reverse()) {
const line = JSON.stringify(message)
size += line.length
if (size > 32_000) break
lines.unshift(line)
}
const event = await writer(DirectMessage)
.setContent(lines.join("\n"))
.addRecipient(PLATFORM_LOGEE)
.renderTemplate()
return wraps.get().publish({event, recipients: [PLATFORM_LOGEE], pow: 16})
}

View file

@ -26,7 +26,6 @@ export type SettingsValues = {
hide_sensitive: boolean
trusted_relays: string[]
report_usage: boolean
report_errors: boolean
relay_auth: RelayAuthMode
send_delay: number
font_size: number
@ -39,7 +38,6 @@ export const defaultSettings: SettingsValues = {
hide_sensitive: true,
trusted_relays: [],
report_usage: true,
report_errors: true,
relay_auth: RelayAuthMode.Conservative,
send_delay: 0,
font_size: 1.1,

View file

@ -21,6 +21,7 @@
import {app} from "@app/core"
import {setupHistory} from "@app/routes"
import {setupAnalytics} from "@app/analytics"
import {setupLogging} from "@app/logger"
import "@app/policies"
import {restoreSession} from "@app/session"
import {signerRequests} from "@app/signer"
@ -128,6 +129,9 @@
// Attach the user before anything reads or decrypts on their behalf
unsubscribers.push(await restoreSession())
// Set up logging
unsubscribers.push(setupLogging())
// Sync stuff to storage
await Promise.all([
sync({

View file

@ -7,14 +7,33 @@
import Card from "@lib/components/Card.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import {pushToast} from "@app/toast"
import {PLATFORM_NAME} from "@app/env"
import {sendLogs} from "@app/logger"
import {RelayAuthMode, userSettingsValues, publishSettings} from "@app/settings"
const reset = () => {
settings = {...$userSettingsValues}
}
const send = async () => {
loading = true
try {
const thunk = await sendLogs()
const error = await thunk.waitForError()
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Your logs have been sent. Thank you!"})
}
} finally {
loading = false
}
}
const onAuthModeChange = (checked: boolean) => {
settings.relay_auth = checked ? RelayAuthMode.Aggressive : RelayAuthMode.Conservative
}
@ -26,6 +45,7 @@
})
let settings = $state({...$userSettingsValues})
let loading = $state(false)
</script>
<form {onsubmit}>
@ -48,17 +68,6 @@
<p>Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.</p>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Report errors?</p>
{/snippet}
{#snippet input()}
<ToggleInput bind:checked={settings.report_errors} />
{/snippet}
{#snippet info()}
<p>Allow {PLATFORM_NAME} to send error reports to help improve the app.</p>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Report usage?</p>
@ -70,6 +79,21 @@
<p>Allow {PLATFORM_NAME} to collect anonymous usage data.</p>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Something went wrong?</p>
{/snippet}
{#snippet input()}
<Button class="button button-neutral" onclick={send} disabled={loading}>
<Spinner {loading}>Send Logs</Spinner>
</Button>
{/snippet}
{#snippet info()}
<p>
Send a record of recent app activity to the {PLATFORM_NAME} team as a direct message.
</p>
{/snippet}
</FieldInline>
</Card>
<Card class="sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
<Button class="button button-neutral" onclick={reset}>Discard Changes</Button>