diff --git a/.env b/.env index de1082be..4e87c8eb 100644 --- a/.env +++ b/.env @@ -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." diff --git a/README.md b/README.md index 618baaa4..29d5886f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/app/core.ts b/src/app/core.ts index d79c6f80..cd1d3c51 100644 --- a/src/app/core.ts +++ b/src/app/core.ts @@ -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) diff --git a/src/app/env.ts b/src/app/env.ts index d4e45d1d..1d9a02de 100644 --- a/src/app/env.ts +++ b/src/app/env.ts @@ -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 diff --git a/src/app/logger.ts b/src/app/logger.ts new file mode 100644 index 00000000..31ef4fd8 --- /dev/null +++ b/src/app/logger.ts @@ -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 + + 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}) +} diff --git a/src/app/settings.ts b/src/app/settings.ts index 8e43e808..5d97d1ca 100644 --- a/src/app/settings.ts +++ b/src/app/settings.ts @@ -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, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dece46dc..5cbc1be9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -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({ diff --git a/src/routes/settings/privacy/+page.svelte b/src/routes/settings/privacy/+page.svelte index 8a395a4f..6a613a94 100644 --- a/src/routes/settings/privacy/+page.svelte +++ b/src/routes/settings/privacy/+page.svelte @@ -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)
@@ -48,17 +68,6 @@

Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.

{/snippet} - - {#snippet label()} -

Report errors?

- {/snippet} - {#snippet input()} - - {/snippet} - {#snippet info()} -

Allow {PLATFORM_NAME} to send error reports to help improve the app.

- {/snippet} -
{#snippet label()}

Report usage?

@@ -70,6 +79,21 @@

Allow {PLATFORM_NAME} to collect anonymous usage data.

{/snippet}
+ + {#snippet label()} +

Something went wrong?

+ {/snippet} + {#snippet input()} + + {/snippet} + {#snippet info()} +

+ Send a record of recent app activity to the {PLATFORM_NAME} team as a direct message. +

+ {/snippet} +