TidGi-Desktop/docs/Analytics.md

5.2 KiB

Analytics in TidGi Desktop

This document explains how analytics works in TidGi Desktop, what is intentionally tracked, what is intentionally blocked, and how plugin authors should integrate with the analytics service.

Goals

TidGi uses analytics to understand coarse product usage without collecting user content.

The current design goals are:

  • Keep all network delivery in the Electron main process
  • Never expose the analytics API key to renderer or plugin code
  • Track only coarse, product-level behavior
  • Reject free-form content, note text, file paths, URLs, tokens, and other sensitive payloads
  • Give plugin authors a stable way to emit custom product events without bypassing privacy guardrails

Architecture

TidGi does not initialize a browser-side analytics SDK.

Instead:

  1. Renderer code and TiddlyWiki plugins call the TidGi analytics service through the existing IPC proxy layer
  2. The analytics service runs in the main process
  3. The main process sends events to Rybbit over HTTP

Relevant files:

  • src/services/analytics/interface.ts
  • src/services/analytics/index.ts
  • src/preload/common/services.ts
  • src/preload/common/exportServices.ts
  • src/services/wiki/plugin/ipcSyncAdaptor/Startup/mount-tidgi-service.ts

Delivery model

  • Analytics is enabled only when all of the following are true:
    • analyticsEnabled preference is true
    • analyticsHost is configured
    • analyticsSiteId is configured
    • a main-process-only analytics API key is configured
  • Unsent events may be queued temporarily in memory
  • The queue is dropped immediately when the user disables analytics
  • The first-run disclosure is tracked separately from normal product events

Privacy constraints

TidGi analytics must never contain:

  • note titles or note bodies
  • workspace names
  • filesystem paths
  • raw URLs
  • access tokens, OAuth codes, cookies, or API keys
  • free-form user text copied from the UI

If a proposed event depends on any of the above, do not add it to analytics.

Built-in events

The application currently emits built-in events such as:

  • app.launched
  • analytics.disclosure_dismissed
  • workspace.created
  • workspace.activated
  • preferences.analytics_updated
  • settings.opened
  • theme.changed
  • deep_link.opened
  • sync.triggered
  • sync.completed
  • sync.failed
  • updater.check_started
  • updater.update_available
  • updater.update_not_available
  • updater.check_failed
  • error.report_requested

Built-in events use an allowlist. For each built-in event, only explicitly approved property keys are retained.

That allowlist lives in src/services/analytics/index.ts.

Plugin-defined custom events

Plugin authors must not emit arbitrary event names through the low-level built-in event contract.

Instead, plugins should call:

await window.service.analytics.trackPluginEvent(pluginId, eventName, properties);

or inside a TiddlyWiki plugin:

await $tw.tidgi.service.analytics.trackPluginEvent(pluginId, eventName, properties);

Final event name format

The service converts plugin calls into this final event name:

plugin.<pluginId>.<eventName>

Example:

await window.service.analytics.trackPluginEvent('kanban-board', 'card_created', {
  source: 'toolbar',
  has_due_date: true,
});

Emits:

plugin.kanban-board.card_created

Validation rules

Plugin event names are intentionally restricted.

  • pluginId must match: ^[a-z0-9]+(?:[-_][a-z0-9]+)*$
  • eventName must match: ^[a-z0-9]+(?:[-_][a-z0-9]+)*$
  • Property keys must match: ^[a-z][a-z0-9_]{0,39}$
  • Property values must be string | number | boolean
  • String values are truncated to 120 characters

If pluginId or eventName is invalid, the event is rejected.

If all properties are invalid, the event is still allowed to be sent without properties.

What plugin authors should track

Good examples:

  • whether a plugin feature was used
  • which plugin surface triggered an action (toolbar, context_menu, shortcut)
  • coarse booleans like has_filter, used_template, has_due_date
  • bounded enums represented as short strings

Bad examples:

  • card title text
  • search query text
  • raw wiki URL
  • tiddler title
  • workspace name
  • exported content

When to add a built-in event instead of a plugin event

Use a built-in event when the behavior is part of TidGi core product behavior and should be governed by a fixed property allowlist in source control.

Use trackPluginEvent() when the event belongs to a plugin or extension and needs a stable but bounded custom namespace.

Rybbit usage in TidGi today

Today TidGi only uses Rybbit's event ingestion path for coarse custom events.

The current implementation sends HTTP requests to the configured Rybbit host using the server-side API key stored only in the main process.

Verification checklist for analytics changes

When editing analytics behavior:

  1. Confirm the event does not include content or identifiers from user data
  2. If it is a built-in event, update the property allowlist
  3. If it is a plugin event, prefer trackPluginEvent() instead of widening built-in types
  4. Run pnpm check
  5. Run eslint on changed files
  6. Update this document and docs/TidGiServiceAPI.md if the callable surface changed