From c6bab4b8b25d28059865f70a432fe356811c4374 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 15:52:54 +0200 Subject: [PATCH 01/16] Update directory structure for Zensible --- .github/workflows/docs.yml | 29 +++ docs.11tydata.js | 12 -- {api => docs/api}/api-query-from-insight.md | 0 {api => docs/api}/api-run-query.md | 0 {api => docs/api}/api-token.md | 0 {api => docs/api}/api.json | 0 {api => docs/api}/insights-reference.md | 0 {api => docs/api}/signals-reference.md | 0 .../articles}/anonymization-how-it-works.md | 0 .../articles}/app-privacy-report.md | 0 .../articles}/app-tracking-transparency.md | 0 .../articles}/apple-app-privacy.md | 0 {articles => docs/articles}/articles.json | 0 ...-if-users-upgrade-to-latest-app-version.md | 0 .../articles}/create-custom-dashboards.md | 0 .../articles}/decide-to-drop-ios-version.md | 0 {articles => docs/articles}/documentation.md | 0 .../articles}/duration-signals.md | 0 {articles => docs/articles}/grand-rename.md | 0 .../articles}/hosting-solutions.md | 0 .../articles}/how-to-funnel-insights.md | 0 .../articles}/insights-about-daily-users.md | 0 .../articles}/insights-about-monthly-users.md | 0 .../articles}/insights-about-referrers.md | 0 .../insights-about-system-version.md | 0 {articles => docs/articles}/insights.md | 0 .../invite-members-to-organization.md | 0 {articles => docs/articles}/login-options.md | 0 {articles => docs/articles}/making-account.md | 0 {articles => docs/articles}/namespaces.md | 0 .../articles}/navigation-signals.md | 0 {articles => docs/articles}/notebooks.md | 0 {articles => docs/articles}/preset-errors.md | 0 .../articles}/preset-purchases.md | 0 .../articles}/set-up-filters-insights.md | 0 .../articles}/signal-type-naming.md | 0 .../articles}/telemetry-client.md | 0 {articles => docs/articles}/test-mode.md | 0 {articles => docs/articles}/update-package.md | 0 {basics => docs/basics}/acquisition.md | 0 {basics => docs/basics}/activation.md | 0 {basics => docs/basics}/basics.json | 0 {basics => docs/basics}/index.md | 0 {basics => docs/basics}/metrics.md | 0 {basics => docs/basics}/pirate-metrics.md | 0 {basics => docs/basics}/retention.md | 0 glossary.md => docs/glossary.md | 0 {guides => docs/guides}/android-setup.md | 0 {guides => docs/guides}/flutter-setup.md | 0 {guides => docs/guides}/guides.json | 0 {guides => docs/guides}/javascript-setup.md | 0 {guides => docs/guides}/objective-c-setup.md | 0 {guides => docs/guides}/privacy-faq.md | 0 {guides => docs/guides}/react-setup.md | 0 {guides => docs/guides}/swift-setup.md | 0 {guides => docs/guides}/vue-setup.md | 0 {guides => docs/guides}/web-setup.md | 0 docs/index.md | 181 ++++++++++++++++++ index.njk => docs/index.njk | 0 {ingest => docs/ingest}/default-parameters.md | 0 {ingest => docs/ingest}/ingest.json | 0 {ingest => docs/ingest}/v1.md | 0 {ingest => docs/ingest}/v2.md | 0 .../integrations}/integrations.json | 0 .../integrations}/revenuecat.md | 0 .../integrations}/superwall.md | 0 .../web-setup-google-tag-manager.md | 0 docs/markdown.md | 111 +++++++++++ .../recipes}/averagaging-numbers.md | 0 {recipes => docs/recipes}/listing-events.md | 0 {recipes => docs/recipes}/recipes.json | 0 .../recipes}/views-per-purchase-example.md | 0 {tql => docs/tql}/aggregators.md | 0 {tql => docs/tql}/baseFilters.md | 0 {tql => docs/tql}/descending.md | 0 {tql => docs/tql}/dimensionSpec.md | 0 {tql => docs/tql}/experiment.md | 0 {tql => docs/tql}/extractionFunction.md | 0 {tql => docs/tql}/filters.md | 0 {tql => docs/tql}/firstGuideline.md | 0 {tql => docs/tql}/funnel.md | 0 {tql => docs/tql}/funnels.md | 0 {tql => docs/tql}/granularity.md | 0 {tql => docs/tql}/groupBy.md | 0 {tql => docs/tql}/post-aggregators.md | 0 {tql => docs/tql}/query.md | 0 {tql => docs/tql}/queryContext.md | 0 {tql => docs/tql}/queryType.md | 0 {tql => docs/tql}/retention.md | 0 {tql => docs/tql}/scan.md | 0 {tql => docs/tql}/time-intervals.md | 0 {tql => docs/tql}/timeseries.md | 0 {tql => docs/tql}/topN.md | 0 {tql => docs/tql}/topNMetricSpec.md | 0 {tql => docs/tql}/tql.json | 0 {tql => docs/tql}/valueFormatter.md | 0 96 files changed, 321 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs.11tydata.js rename {api => docs/api}/api-query-from-insight.md (100%) rename {api => docs/api}/api-run-query.md (100%) rename {api => docs/api}/api-token.md (100%) rename {api => docs/api}/api.json (100%) rename {api => docs/api}/insights-reference.md (100%) rename {api => docs/api}/signals-reference.md (100%) rename {articles => docs/articles}/anonymization-how-it-works.md (100%) rename {articles => docs/articles}/app-privacy-report.md (100%) rename {articles => docs/articles}/app-tracking-transparency.md (100%) rename {articles => docs/articles}/apple-app-privacy.md (100%) rename {articles => docs/articles}/articles.json (100%) rename {articles => docs/articles}/check-if-users-upgrade-to-latest-app-version.md (100%) rename {articles => docs/articles}/create-custom-dashboards.md (100%) rename {articles => docs/articles}/decide-to-drop-ios-version.md (100%) rename {articles => docs/articles}/documentation.md (100%) rename {articles => docs/articles}/duration-signals.md (100%) rename {articles => docs/articles}/grand-rename.md (100%) rename {articles => docs/articles}/hosting-solutions.md (100%) rename {articles => docs/articles}/how-to-funnel-insights.md (100%) rename {articles => docs/articles}/insights-about-daily-users.md (100%) rename {articles => docs/articles}/insights-about-monthly-users.md (100%) rename {articles => docs/articles}/insights-about-referrers.md (100%) rename {articles => docs/articles}/insights-about-system-version.md (100%) rename {articles => docs/articles}/insights.md (100%) rename {articles => docs/articles}/invite-members-to-organization.md (100%) rename {articles => docs/articles}/login-options.md (100%) rename {articles => docs/articles}/making-account.md (100%) rename {articles => docs/articles}/namespaces.md (100%) rename {articles => docs/articles}/navigation-signals.md (100%) rename {articles => docs/articles}/notebooks.md (100%) rename {articles => docs/articles}/preset-errors.md (100%) rename {articles => docs/articles}/preset-purchases.md (100%) rename {articles => docs/articles}/set-up-filters-insights.md (100%) rename {articles => docs/articles}/signal-type-naming.md (100%) rename {articles => docs/articles}/telemetry-client.md (100%) rename {articles => docs/articles}/test-mode.md (100%) rename {articles => docs/articles}/update-package.md (100%) rename {basics => docs/basics}/acquisition.md (100%) rename {basics => docs/basics}/activation.md (100%) rename {basics => docs/basics}/basics.json (100%) rename {basics => docs/basics}/index.md (100%) rename {basics => docs/basics}/metrics.md (100%) rename {basics => docs/basics}/pirate-metrics.md (100%) rename {basics => docs/basics}/retention.md (100%) rename glossary.md => docs/glossary.md (100%) rename {guides => docs/guides}/android-setup.md (100%) rename {guides => docs/guides}/flutter-setup.md (100%) rename {guides => docs/guides}/guides.json (100%) rename {guides => docs/guides}/javascript-setup.md (100%) rename {guides => docs/guides}/objective-c-setup.md (100%) rename {guides => docs/guides}/privacy-faq.md (100%) rename {guides => docs/guides}/react-setup.md (100%) rename {guides => docs/guides}/swift-setup.md (100%) rename {guides => docs/guides}/vue-setup.md (100%) rename {guides => docs/guides}/web-setup.md (100%) create mode 100644 docs/index.md rename index.njk => docs/index.njk (100%) rename {ingest => docs/ingest}/default-parameters.md (100%) rename {ingest => docs/ingest}/ingest.json (100%) rename {ingest => docs/ingest}/v1.md (100%) rename {ingest => docs/ingest}/v2.md (100%) rename {integrations => docs/integrations}/integrations.json (100%) rename {integrations => docs/integrations}/revenuecat.md (100%) rename {integrations => docs/integrations}/superwall.md (100%) rename {integrations => docs/integrations}/web-setup-google-tag-manager.md (100%) create mode 100644 docs/markdown.md rename {recipes => docs/recipes}/averagaging-numbers.md (100%) rename {recipes => docs/recipes}/listing-events.md (100%) rename {recipes => docs/recipes}/recipes.json (100%) rename {recipes => docs/recipes}/views-per-purchase-example.md (100%) rename {tql => docs/tql}/aggregators.md (100%) rename {tql => docs/tql}/baseFilters.md (100%) rename {tql => docs/tql}/descending.md (100%) rename {tql => docs/tql}/dimensionSpec.md (100%) rename {tql => docs/tql}/experiment.md (100%) rename {tql => docs/tql}/extractionFunction.md (100%) rename {tql => docs/tql}/filters.md (100%) rename {tql => docs/tql}/firstGuideline.md (100%) rename {tql => docs/tql}/funnel.md (100%) rename {tql => docs/tql}/funnels.md (100%) rename {tql => docs/tql}/granularity.md (100%) rename {tql => docs/tql}/groupBy.md (100%) rename {tql => docs/tql}/post-aggregators.md (100%) rename {tql => docs/tql}/query.md (100%) rename {tql => docs/tql}/queryContext.md (100%) rename {tql => docs/tql}/queryType.md (100%) rename {tql => docs/tql}/retention.md (100%) rename {tql => docs/tql}/scan.md (100%) rename {tql => docs/tql}/time-intervals.md (100%) rename {tql => docs/tql}/timeseries.md (100%) rename {tql => docs/tql}/topN.md (100%) rename {tql => docs/tql}/topNMetricSpec.md (100%) rename {tql => docs/tql}/tql.json (100%) rename {tql => docs/tql}/valueFormatter.md (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..47ee722 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,29 @@ +name: Documentation +on: + push: + branches: + - master + - main +permissions: + contents: read + pages: write + id-token: write +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: pip install zensical + - run: zensical build --clean + - uses: actions/upload-pages-artifact@v5 + with: + path: site + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/docs.11tydata.js b/docs.11tydata.js deleted file mode 100644 index e10251a..0000000 --- a/docs.11tydata.js +++ /dev/null @@ -1,12 +0,0 @@ -let data = { - layout: "docs/docpage.njk", - tags: "docs", - sitemapchangefrequency: "daily", - sitemappriority: "0.7", -}; - -// if(process.env.NODE_ENV === "production") { -data.date = "git Last Modified"; -// } - -module.exports = data; diff --git a/api/api-query-from-insight.md b/docs/api/api-query-from-insight.md similarity index 100% rename from api/api-query-from-insight.md rename to docs/api/api-query-from-insight.md diff --git a/api/api-run-query.md b/docs/api/api-run-query.md similarity index 100% rename from api/api-run-query.md rename to docs/api/api-run-query.md diff --git a/api/api-token.md b/docs/api/api-token.md similarity index 100% rename from api/api-token.md rename to docs/api/api-token.md diff --git a/api/api.json b/docs/api/api.json similarity index 100% rename from api/api.json rename to docs/api/api.json diff --git a/api/insights-reference.md b/docs/api/insights-reference.md similarity index 100% rename from api/insights-reference.md rename to docs/api/insights-reference.md diff --git a/api/signals-reference.md b/docs/api/signals-reference.md similarity index 100% rename from api/signals-reference.md rename to docs/api/signals-reference.md diff --git a/articles/anonymization-how-it-works.md b/docs/articles/anonymization-how-it-works.md similarity index 100% rename from articles/anonymization-how-it-works.md rename to docs/articles/anonymization-how-it-works.md diff --git a/articles/app-privacy-report.md b/docs/articles/app-privacy-report.md similarity index 100% rename from articles/app-privacy-report.md rename to docs/articles/app-privacy-report.md diff --git a/articles/app-tracking-transparency.md b/docs/articles/app-tracking-transparency.md similarity index 100% rename from articles/app-tracking-transparency.md rename to docs/articles/app-tracking-transparency.md diff --git a/articles/apple-app-privacy.md b/docs/articles/apple-app-privacy.md similarity index 100% rename from articles/apple-app-privacy.md rename to docs/articles/apple-app-privacy.md diff --git a/articles/articles.json b/docs/articles/articles.json similarity index 100% rename from articles/articles.json rename to docs/articles/articles.json diff --git a/articles/check-if-users-upgrade-to-latest-app-version.md b/docs/articles/check-if-users-upgrade-to-latest-app-version.md similarity index 100% rename from articles/check-if-users-upgrade-to-latest-app-version.md rename to docs/articles/check-if-users-upgrade-to-latest-app-version.md diff --git a/articles/create-custom-dashboards.md b/docs/articles/create-custom-dashboards.md similarity index 100% rename from articles/create-custom-dashboards.md rename to docs/articles/create-custom-dashboards.md diff --git a/articles/decide-to-drop-ios-version.md b/docs/articles/decide-to-drop-ios-version.md similarity index 100% rename from articles/decide-to-drop-ios-version.md rename to docs/articles/decide-to-drop-ios-version.md diff --git a/articles/documentation.md b/docs/articles/documentation.md similarity index 100% rename from articles/documentation.md rename to docs/articles/documentation.md diff --git a/articles/duration-signals.md b/docs/articles/duration-signals.md similarity index 100% rename from articles/duration-signals.md rename to docs/articles/duration-signals.md diff --git a/articles/grand-rename.md b/docs/articles/grand-rename.md similarity index 100% rename from articles/grand-rename.md rename to docs/articles/grand-rename.md diff --git a/articles/hosting-solutions.md b/docs/articles/hosting-solutions.md similarity index 100% rename from articles/hosting-solutions.md rename to docs/articles/hosting-solutions.md diff --git a/articles/how-to-funnel-insights.md b/docs/articles/how-to-funnel-insights.md similarity index 100% rename from articles/how-to-funnel-insights.md rename to docs/articles/how-to-funnel-insights.md diff --git a/articles/insights-about-daily-users.md b/docs/articles/insights-about-daily-users.md similarity index 100% rename from articles/insights-about-daily-users.md rename to docs/articles/insights-about-daily-users.md diff --git a/articles/insights-about-monthly-users.md b/docs/articles/insights-about-monthly-users.md similarity index 100% rename from articles/insights-about-monthly-users.md rename to docs/articles/insights-about-monthly-users.md diff --git a/articles/insights-about-referrers.md b/docs/articles/insights-about-referrers.md similarity index 100% rename from articles/insights-about-referrers.md rename to docs/articles/insights-about-referrers.md diff --git a/articles/insights-about-system-version.md b/docs/articles/insights-about-system-version.md similarity index 100% rename from articles/insights-about-system-version.md rename to docs/articles/insights-about-system-version.md diff --git a/articles/insights.md b/docs/articles/insights.md similarity index 100% rename from articles/insights.md rename to docs/articles/insights.md diff --git a/articles/invite-members-to-organization.md b/docs/articles/invite-members-to-organization.md similarity index 100% rename from articles/invite-members-to-organization.md rename to docs/articles/invite-members-to-organization.md diff --git a/articles/login-options.md b/docs/articles/login-options.md similarity index 100% rename from articles/login-options.md rename to docs/articles/login-options.md diff --git a/articles/making-account.md b/docs/articles/making-account.md similarity index 100% rename from articles/making-account.md rename to docs/articles/making-account.md diff --git a/articles/namespaces.md b/docs/articles/namespaces.md similarity index 100% rename from articles/namespaces.md rename to docs/articles/namespaces.md diff --git a/articles/navigation-signals.md b/docs/articles/navigation-signals.md similarity index 100% rename from articles/navigation-signals.md rename to docs/articles/navigation-signals.md diff --git a/articles/notebooks.md b/docs/articles/notebooks.md similarity index 100% rename from articles/notebooks.md rename to docs/articles/notebooks.md diff --git a/articles/preset-errors.md b/docs/articles/preset-errors.md similarity index 100% rename from articles/preset-errors.md rename to docs/articles/preset-errors.md diff --git a/articles/preset-purchases.md b/docs/articles/preset-purchases.md similarity index 100% rename from articles/preset-purchases.md rename to docs/articles/preset-purchases.md diff --git a/articles/set-up-filters-insights.md b/docs/articles/set-up-filters-insights.md similarity index 100% rename from articles/set-up-filters-insights.md rename to docs/articles/set-up-filters-insights.md diff --git a/articles/signal-type-naming.md b/docs/articles/signal-type-naming.md similarity index 100% rename from articles/signal-type-naming.md rename to docs/articles/signal-type-naming.md diff --git a/articles/telemetry-client.md b/docs/articles/telemetry-client.md similarity index 100% rename from articles/telemetry-client.md rename to docs/articles/telemetry-client.md diff --git a/articles/test-mode.md b/docs/articles/test-mode.md similarity index 100% rename from articles/test-mode.md rename to docs/articles/test-mode.md diff --git a/articles/update-package.md b/docs/articles/update-package.md similarity index 100% rename from articles/update-package.md rename to docs/articles/update-package.md diff --git a/basics/acquisition.md b/docs/basics/acquisition.md similarity index 100% rename from basics/acquisition.md rename to docs/basics/acquisition.md diff --git a/basics/activation.md b/docs/basics/activation.md similarity index 100% rename from basics/activation.md rename to docs/basics/activation.md diff --git a/basics/basics.json b/docs/basics/basics.json similarity index 100% rename from basics/basics.json rename to docs/basics/basics.json diff --git a/basics/index.md b/docs/basics/index.md similarity index 100% rename from basics/index.md rename to docs/basics/index.md diff --git a/basics/metrics.md b/docs/basics/metrics.md similarity index 100% rename from basics/metrics.md rename to docs/basics/metrics.md diff --git a/basics/pirate-metrics.md b/docs/basics/pirate-metrics.md similarity index 100% rename from basics/pirate-metrics.md rename to docs/basics/pirate-metrics.md diff --git a/basics/retention.md b/docs/basics/retention.md similarity index 100% rename from basics/retention.md rename to docs/basics/retention.md diff --git a/glossary.md b/docs/glossary.md similarity index 100% rename from glossary.md rename to docs/glossary.md diff --git a/guides/android-setup.md b/docs/guides/android-setup.md similarity index 100% rename from guides/android-setup.md rename to docs/guides/android-setup.md diff --git a/guides/flutter-setup.md b/docs/guides/flutter-setup.md similarity index 100% rename from guides/flutter-setup.md rename to docs/guides/flutter-setup.md diff --git a/guides/guides.json b/docs/guides/guides.json similarity index 100% rename from guides/guides.json rename to docs/guides/guides.json diff --git a/guides/javascript-setup.md b/docs/guides/javascript-setup.md similarity index 100% rename from guides/javascript-setup.md rename to docs/guides/javascript-setup.md diff --git a/guides/objective-c-setup.md b/docs/guides/objective-c-setup.md similarity index 100% rename from guides/objective-c-setup.md rename to docs/guides/objective-c-setup.md diff --git a/guides/privacy-faq.md b/docs/guides/privacy-faq.md similarity index 100% rename from guides/privacy-faq.md rename to docs/guides/privacy-faq.md diff --git a/guides/react-setup.md b/docs/guides/react-setup.md similarity index 100% rename from guides/react-setup.md rename to docs/guides/react-setup.md diff --git a/guides/swift-setup.md b/docs/guides/swift-setup.md similarity index 100% rename from guides/swift-setup.md rename to docs/guides/swift-setup.md diff --git a/guides/vue-setup.md b/docs/guides/vue-setup.md similarity index 100% rename from guides/vue-setup.md rename to docs/guides/vue-setup.md diff --git a/guides/web-setup.md b/docs/guides/web-setup.md similarity index 100% rename from guides/web-setup.md rename to docs/guides/web-setup.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b50d7a1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,181 @@ +--- +icon: lucide/rocket +--- + +!!! warning + + This is a test for a new documentation system. Trust nothing and no one. The original TelemetryDeck + documentation still lives at https://telemetrydeck.com/docs/ for now. + + +---- + +# Get started + +For full documentation visit [zensical.org](https://zensical.org/docs/). + +## Commands + +* [`zensical new`][new] - Create a new project +* [`zensical serve`][serve] - Start local web server +* [`zensical build`][build] - Build your site + + [new]: https://zensical.org/docs/usage/new/ + [serve]: https://zensical.org/docs/usage/preview/ + [build]: https://zensical.org/docs/usage/build/ + +## Examples + +### Admonitions + +> Go to [documentation](https://zensical.org/docs/authoring/admonitions/) + +!!! note + + This is a **note** admonition. Use it to provide helpful information. + +!!! warning + + This is a **warning** admonition. Be careful! + +### Details + +> Go to [documentation](https://zensical.org/docs/authoring/admonitions/#collapsible-blocks) + +??? info "Click to expand for more info" + + This content is hidden until you click to expand it. + Great for FAQs or long explanations. + +## Code Blocks + +> Go to [documentation](https://zensical.org/docs/authoring/code-blocks/) + +``` python hl_lines="2" title="Code blocks" +def greet(name): + print(f"Hello, {name}!") # (1)! + +greet("Python") +``` + +1. > Go to [documentation](https://zensical.org/docs/authoring/code-blocks/#code-annotations) + + Code annotations allow to attach notes to lines of code. + +Code can also be highlighted inline: `#!python print("Hello, Python!")`. + +## Content tabs + +> Go to [documentation](https://zensical.org/docs/authoring/content-tabs/) + +=== "Python" + + ``` python + print("Hello from Python!") + ``` + +=== "Rust" + + ``` rs + println!("Hello from Rust!"); + ``` + +## Diagrams + +> Go to [documentation](https://zensical.org/docs/authoring/diagrams/) + +``` mermaid +graph LR + A[Start] --> B{Error?}; + B -->|Yes| C[Hmm...]; + C --> D[Debug]; + D --> B; + B ---->|No| E[Yay!]; +``` + +## Footnotes + +> Go to [documentation](https://zensical.org/docs/authoring/footnotes/) + +Here's a sentence with a footnote.[^1] + +Hover it, to see a tooltip. + +[^1]: This is the footnote. + + +## Formatting + +> Go to [documentation](https://zensical.org/docs/authoring/formatting/) + +- ==This was marked (highlight)== +- ^^This was inserted (underline)^^ +- ~~This was deleted (strikethrough)~~ +- H~2~O +- A^T^A +- ++ctrl+alt+del++ + +## Icons, Emojis + +> Go to [documentation](https://zensical.org/docs/authoring/icons-emojis/) + +* :sparkles: `:sparkles:` +* :rocket: `:rocket:` +* :tada: `:tada:` +* :memo: `:memo:` +* :eyes: `:eyes:` + +## Maths + +> Go to [documentation](https://zensical.org/docs/authoring/math/) + +$$ +\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k} +$$ + +!!! warning "Needs configuration" + Note that MathJax is included via a `script` tag on this page and is not + configured in the generated default configuration to avoid including it + in a pages that do not need it. See the documentation for details on how + to configure it on all your pages if they are more Maths-heavy than these + simple starter pages. + + + + +## Task Lists + +> Go to [documentation](https://zensical.org/docs/authoring/lists/#using-task-lists) + +* [x] Install Zensical +* [x] Configure `zensical.toml` +* [x] Write amazing documentation +* [ ] Deploy anywhere + +## Tooltips + +> Go to [documentation](https://zensical.org/docs/authoring/tooltips/) + +[Hover me][example] + + [example]: https://example.com "I'm a tooltip!" diff --git a/index.njk b/docs/index.njk similarity index 100% rename from index.njk rename to docs/index.njk diff --git a/ingest/default-parameters.md b/docs/ingest/default-parameters.md similarity index 100% rename from ingest/default-parameters.md rename to docs/ingest/default-parameters.md diff --git a/ingest/ingest.json b/docs/ingest/ingest.json similarity index 100% rename from ingest/ingest.json rename to docs/ingest/ingest.json diff --git a/ingest/v1.md b/docs/ingest/v1.md similarity index 100% rename from ingest/v1.md rename to docs/ingest/v1.md diff --git a/ingest/v2.md b/docs/ingest/v2.md similarity index 100% rename from ingest/v2.md rename to docs/ingest/v2.md diff --git a/integrations/integrations.json b/docs/integrations/integrations.json similarity index 100% rename from integrations/integrations.json rename to docs/integrations/integrations.json diff --git a/integrations/revenuecat.md b/docs/integrations/revenuecat.md similarity index 100% rename from integrations/revenuecat.md rename to docs/integrations/revenuecat.md diff --git a/integrations/superwall.md b/docs/integrations/superwall.md similarity index 100% rename from integrations/superwall.md rename to docs/integrations/superwall.md diff --git a/integrations/web-setup-google-tag-manager.md b/docs/integrations/web-setup-google-tag-manager.md similarity index 100% rename from integrations/web-setup-google-tag-manager.md rename to docs/integrations/web-setup-google-tag-manager.md diff --git a/docs/markdown.md b/docs/markdown.md new file mode 100644 index 0000000..e2da6dc --- /dev/null +++ b/docs/markdown.md @@ -0,0 +1,111 @@ +--- +icon: simple/markdown +--- + +# Markdown in 5min + +## Headers + +``` +# H1 Header +## H2 Header +### H3 Header +#### H4 Header +##### H5 Header +###### H6 Header +``` + +## Text formatting + +``` +**bold text** +*italic text* +***bold and italic*** +~~strikethrough~~ +`inline code` +``` + +## Links and images + +``` +[Link text](https://example.com) +[Link with title](https://example.com "Hover title") +![Alt text](image.jpg) +![Image with title](image.jpg "Image title") +``` + +## Lists + +``` +Unordered: + +- Item 1 +- Item 2 + - Nested item + +Ordered: + +1. First item +2. Second item +3. Third item +``` + +## Blockquotes + +``` +> This is a blockquote +> Multiple lines +>> Nested quote +``` + +## Code blocks + +```` +```javascript +function hello() { + console.log("Hello, world!"); +} +``` +```` + +## Tables + +``` +| Header 1 | Header 2 | Header 3 | +|----------|----------|----------| +| Row 1 | Data | Data | +| Row 2 | Data | Data | +``` + +## Horizontal rule + +``` +--- +or +*** +or +___ +``` + +## Task lists + +``` +- [x] Completed task +- [ ] Incomplete task +- [ ] Another task +``` + +## Escaping characters + +``` +Use backslash to escape: \* \_ \# \` +``` + +## Line breaks + +``` +End a line with two spaces +to create a line break. + +Or use a blank line for a new paragraph. +``` \ No newline at end of file diff --git a/recipes/averagaging-numbers.md b/docs/recipes/averagaging-numbers.md similarity index 100% rename from recipes/averagaging-numbers.md rename to docs/recipes/averagaging-numbers.md diff --git a/recipes/listing-events.md b/docs/recipes/listing-events.md similarity index 100% rename from recipes/listing-events.md rename to docs/recipes/listing-events.md diff --git a/recipes/recipes.json b/docs/recipes/recipes.json similarity index 100% rename from recipes/recipes.json rename to docs/recipes/recipes.json diff --git a/recipes/views-per-purchase-example.md b/docs/recipes/views-per-purchase-example.md similarity index 100% rename from recipes/views-per-purchase-example.md rename to docs/recipes/views-per-purchase-example.md diff --git a/tql/aggregators.md b/docs/tql/aggregators.md similarity index 100% rename from tql/aggregators.md rename to docs/tql/aggregators.md diff --git a/tql/baseFilters.md b/docs/tql/baseFilters.md similarity index 100% rename from tql/baseFilters.md rename to docs/tql/baseFilters.md diff --git a/tql/descending.md b/docs/tql/descending.md similarity index 100% rename from tql/descending.md rename to docs/tql/descending.md diff --git a/tql/dimensionSpec.md b/docs/tql/dimensionSpec.md similarity index 100% rename from tql/dimensionSpec.md rename to docs/tql/dimensionSpec.md diff --git a/tql/experiment.md b/docs/tql/experiment.md similarity index 100% rename from tql/experiment.md rename to docs/tql/experiment.md diff --git a/tql/extractionFunction.md b/docs/tql/extractionFunction.md similarity index 100% rename from tql/extractionFunction.md rename to docs/tql/extractionFunction.md diff --git a/tql/filters.md b/docs/tql/filters.md similarity index 100% rename from tql/filters.md rename to docs/tql/filters.md diff --git a/tql/firstGuideline.md b/docs/tql/firstGuideline.md similarity index 100% rename from tql/firstGuideline.md rename to docs/tql/firstGuideline.md diff --git a/tql/funnel.md b/docs/tql/funnel.md similarity index 100% rename from tql/funnel.md rename to docs/tql/funnel.md diff --git a/tql/funnels.md b/docs/tql/funnels.md similarity index 100% rename from tql/funnels.md rename to docs/tql/funnels.md diff --git a/tql/granularity.md b/docs/tql/granularity.md similarity index 100% rename from tql/granularity.md rename to docs/tql/granularity.md diff --git a/tql/groupBy.md b/docs/tql/groupBy.md similarity index 100% rename from tql/groupBy.md rename to docs/tql/groupBy.md diff --git a/tql/post-aggregators.md b/docs/tql/post-aggregators.md similarity index 100% rename from tql/post-aggregators.md rename to docs/tql/post-aggregators.md diff --git a/tql/query.md b/docs/tql/query.md similarity index 100% rename from tql/query.md rename to docs/tql/query.md diff --git a/tql/queryContext.md b/docs/tql/queryContext.md similarity index 100% rename from tql/queryContext.md rename to docs/tql/queryContext.md diff --git a/tql/queryType.md b/docs/tql/queryType.md similarity index 100% rename from tql/queryType.md rename to docs/tql/queryType.md diff --git a/tql/retention.md b/docs/tql/retention.md similarity index 100% rename from tql/retention.md rename to docs/tql/retention.md diff --git a/tql/scan.md b/docs/tql/scan.md similarity index 100% rename from tql/scan.md rename to docs/tql/scan.md diff --git a/tql/time-intervals.md b/docs/tql/time-intervals.md similarity index 100% rename from tql/time-intervals.md rename to docs/tql/time-intervals.md diff --git a/tql/timeseries.md b/docs/tql/timeseries.md similarity index 100% rename from tql/timeseries.md rename to docs/tql/timeseries.md diff --git a/tql/topN.md b/docs/tql/topN.md similarity index 100% rename from tql/topN.md rename to docs/tql/topN.md diff --git a/tql/topNMetricSpec.md b/docs/tql/topNMetricSpec.md similarity index 100% rename from tql/topNMetricSpec.md rename to docs/tql/topNMetricSpec.md diff --git a/tql/tql.json b/docs/tql/tql.json similarity index 100% rename from tql/tql.json rename to docs/tql/tql.json diff --git a/tql/valueFormatter.md b/docs/tql/valueFormatter.md similarity index 100% rename from tql/valueFormatter.md rename to docs/tql/valueFormatter.md From 40676077deaaee77efda9a8362cf72d9e3a1dee6 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 15:53:05 +0200 Subject: [PATCH 02/16] Add rudimentary zensible configuration file --- zensical.toml | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 zensical.toml diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..be1233c --- /dev/null +++ b/zensical.toml @@ -0,0 +1,349 @@ +# ============================================================================ +# +# The configuration produced by default is meant to highlight the features +# that Zensical provides and to serve as a starting point for your own +# projects. +# +# ============================================================================ + +[project] + +# The site_name is shown in the page header and the browser window title +# +# Read more: https://zensical.org/docs/setup/basics/#site_name +site_name = "TelemetryDeck Documentation" + +# The site_description is included in the HTML head and should contain a +# meaningful description of the site content for use by search engines. +# +# Read more: https://zensical.org/docs/setup/basics/#site_description +site_description = "Documentation for the installation and usage of TelemetryDeck, a privacy first usage analytics service." + +# The site_author attribute. This is used in the HTML head element. +# +# Read more: https://zensical.org/docs/setup/basics/#site_author +site_author = "TelemetryDeck Docs Team" + +# The site_url is the canonical URL for your site. When building online +# documentation you should set this. +# Read more: https://zensical.org/docs/setup/basics/#site_url +#site_url = "https://docs.telemetrydeck.com/" + +# The copyright notice appears in the page footer and can contain an HTML +# fragment. +# +# Read more: https://zensical.org/docs/setup/basics/#copyright +copyright = """ +Copyright © 2026 TelemetryDeck GmbH +""" + +# Zensical supports both implicit navigation and explicitly defined navigation. +# If you decide not to define a navigation here then Zensical will simply +# derive the navigation structure from the directory structure of your +# "docs_dir". The definition below demonstrates how a navigation structure +# can be defined using TOML syntax. +# +# Read more: https://zensical.org/docs/setup/navigation/ +# nav = [ +# { "Get started" = "index.md" }, +# { "Markdown in 5min" = "markdown.md" }, +# ] + +# With the "extra_css" option you can add your own CSS styling to customize +# your Zensical project according to your needs. You can add any number of +# CSS files. +# +# The path provided should be relative to the "docs_dir". +# +# Read more: https://zensical.org/docs/customization/#additional-css +# +#extra_css = ["stylesheets/extra.css"] + +# With the `extra_javascript` option you can add your own JavaScript to your +# project to customize the behavior according to your needs. +# +# The path provided should be relative to the "docs_dir". +# +# Read more: https://zensical.org/docs/customization/#additional-javascript +#extra_javascript = ["javascripts/extra.js"] + +# ---------------------------------------------------------------------------- +# Section for configuring theme options +# ---------------------------------------------------------------------------- +[project.theme] + +# change this to "classic" to use the traditional Material for MkDocs look. +#variant = "classic" + +# Zensical allows you to override specific blocks, partials, or whole +# templates as well as to define your own templates. To do this, uncomment +# the custom_dir setting below and set it to a directory in which you +# keep your template overrides. +# +# Read more: +# - https://zensical.org/docs/customization/#extending-the-theme +# +#custom_dir = "overrides" + +# With the "favicon" option you can set your own image to use as the icon +# browsers will use in the browser title bar or tab bar. The path provided +# must be relative to the "docs_dir". +# +# Read more: +# - https://zensical.org/docs/setup/logo-and-icons/#favicon +# - https://developer.mozilla.org/en-US/docs/Glossary/Favicon +# +#favicon = "images/favicon.png" + +# Zensical supports more than 60 different languages. This means that the +# labels and tooltips that Zensical's templates produce are translated. +# The "language" option allows you to set the language used. This language +# is also indicated in the HTML head element to help with accessibility +# and guide search engines and translation tools. +# +# The default language is "en" (English). It is possible to create +# sites with multiple languages and configure a language selector. See +# the documentation for details. +# +# Read more: +# - https://zensical.org/docs/setup/language/ +# +language = "en" + +# Zensical provides a number of feature toggles that change the behavior +# of the documentation site. +features = [ + # Zensical includes an announcement bar. This feature allows users to + # dismiss it when they have read the announcement. + # https://zensical.org/docs/setup/header/#announcement-bar + "announce.dismiss", + + # If you have a repository configured and turn on this feature, Zensical + # will generate an edit button for the page. This works for common + # repository hosting services. + # https://zensical.org/docs/setup/repository/#content-actions + #"content.action.edit", + + # If you have a repository configured and turn on this feature, Zensical + # will generate a button that allows the user to view the Markdown + # code for the current page. + # https://zensical.org/docs/setup/repository/#content-actions + #"content.action.view", + + # Code annotations allow you to add an icon with a tooltip to your + # code blocks to provide explanations at crucial points. + # https://zensical.org/docs/authoring/code-blocks/#code-annotations + "content.code.annotate", + + # This feature turns on a button in code blocks that allow users to + # copy the content to their clipboard without first selecting it. + # https://zensical.org/docs/authoring/code-blocks/#code-copy-button + "content.code.copy", + + # Code blocks can include a button to allow for the selection of line + # ranges by the user. + # https://zensical.org/docs/authoring/code-blocks/#code-selection-button + "content.code.select", + + # Zensical can render footnotes as inline tooltips, so the user can read + # the footnote without leaving the context of the document. + # https://zensical.org/docs/authoring/footnotes/#footnote-tooltips + "content.footnote.tooltips", + + # If you have many content tabs that have the same titles (e.g., "Python", + # "JavaScript", "Cobol"), this feature causes all of them to switch to + # at the same time when the user chooses their language in one. + # https://zensical.org/docs/authoring/content-tabs/#linked-content-tabs + "content.tabs.link", + + # With this feature enabled users can add tooltips to links that will be + # displayed when the mouse pointer hovers the link. + # https://zensical.org/docs/authoring/tooltips/#improved-tooltips + "content.tooltips", + + # With this feature enabled, Zensical will automatically hide parts + # of the header when the user scrolls past a certain point. + # https://zensical.org/docs/setup/header/#automatic-hiding + # "header.autohide", + + # Turn on this feature to expand all collapsible sections in the + # navigation sidebar by default. + # https://zensical.org/docs/setup/navigation/#navigation-expansion + # "navigation.expand", + + # This feature turns on navigation elements in the footer that allow the + # user to navigate to a next or previous page. + # https://zensical.org/docs/setup/footer/#navigation + "navigation.footer", + + # When section index pages are enabled, documents can be directly attached + # to sections, which is particularly useful for providing overview pages. + # https://zensical.org/docs/setup/navigation/#section-index-pages + "navigation.indexes", + + # When instant navigation is enabled, clicks on all internal links will be + # intercepted and dispatched via XHR without fully reloading the page. + # https://zensical.org/docs/setup/navigation/#instant-navigation + "navigation.instant", + + # With instant prefetching, your site will start to fetch a page once the + # user hovers over a link. This will reduce the perceived loading time + # for the user. + # https://zensical.org/docs/setup/navigation/#instant-prefetching + "navigation.instant.prefetch", + + # In order to provide a better user experience on slow connections when + # using instant navigation, a progress indicator can be enabled. + # https://zensical.org/docs/setup/navigation/#progress-indicator + #"navigation.instant.progress", + + # When navigation paths are activated, a breadcrumb navigation is rendered + # above the title of each page + # https://zensical.org/docs/setup/navigation/#navigation-path + "navigation.path", + + # When pruning is enabled, only the visible navigation items are included + # in the rendered HTML, reducing the size of the built site by 33% or more. + # https://zensical.org/docs/setup/navigation/#navigation-pruning + #"navigation.prune", + + # When sections are enabled, top-level sections are rendered as groups in + # the sidebar for viewports above 1220px, but remain as-is on mobile. + # https://zensical.org/docs/setup/navigation/#navigation-sections + "navigation.sections", + + # When tabs are enabled, top-level sections are rendered in a menu layer + # below the header for viewports above 1220px, but remain as-is on mobile. + # https://zensical.org/docs/setup/navigation/#navigation-tabs + #"navigation.tabs", + + # When sticky tabs are enabled, navigation tabs will lock below the header + # and always remain visible when scrolling down. + # https://zensical.org/docs/setup/navigation/#sticky-navigation-tabs + #"navigation.tabs.sticky", + + # A back-to-top button can be shown when the user, after scrolling down, + # starts to scroll up again. + # https://zensical.org/docs/setup/navigation/#back-to-top-button + "navigation.top", + + # When anchor tracking is enabled, the URL in the address bar is + # automatically updated with the active anchor as highlighted in the table + # of contents. + # https://zensical.org/docs/setup/navigation/#anchor-tracking + "navigation.tracking", + + # When search highlighting is enabled and a user clicks on a search result, + # Zensical will highlight all occurrences after following the link. + # https://zensical.org/docs/setup/search/#search-highlighting + "search.highlight", + + # When anchor following for the table of contents is enabled, the sidebar + # is automatically scrolled so that the active anchor is always visible. + # https://zensical.org/docs/setup/navigation/#anchor-following + # "toc.follow", + + # When navigation integration for the table of contents is enabled, it is + # always rendered as part of the navigation sidebar on the left. + # https://zensical.org/docs/setup/navigation/#navigation-integration + #"toc.integrate", +] + +# ---------------------------------------------------------------------------- +# You can configure your own logo to be shown in the header using the "logo" +# option in the "theme" subsection. The logo must be a relative path to a file +# in your "docs_dir", e.g., to use `docs/assets/logo.png` you would set: +# ---------------------------------------------------------------------------- +#logo = "assets/logo.png" + +# ---------------------------------------------------------------------------- +# If you don't have a dedicated project logo, you can use a built-in icon from +# the icon sets shipped in Zensical. Please note that the setting lives in a +# different subsection, and that the above take precedence over the icon. +# +# Read more: +# - https://zensical.org/docs/setup/logo-and-icons +# - https://github.com/zensical/ui/tree/master/dist/.icons +# ---------------------------------------------------------------------------- +#[project.theme.icon] +#logo = "lucide/smile" + +# ---------------------------------------------------------------------------- +# In the "font" subsection you can configure the fonts used. By default, fonts +# are loaded from Google Fonts, giving you a wide range of choices from a set +# of suitably licensed fonts. There are options for a normal text font and for +# a monospaced font used in code blocks. +# ---------------------------------------------------------------------------- +#[project.theme.font] +#text = "Inter" +#code = "Jetbrains Mono" + +# ---------------------------------------------------------------------------- +# In the "palette" subsection you can configure options for the color scheme. +# You can configure different color schemes, e.g., to turn on dark mode, +# that the user can switch between. Each color scheme can be further +# customized. +# +# Read more: +# - https://zensical.org/docs/setup/colors/ +# ---------------------------------------------------------------------------- +[[project.theme.palette]] +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +# ---------------------------------------------------------------------------- +# The "extra" section contains miscellaneous settings. +# ---------------------------------------------------------------------------- +#[[project.extra.social]] +#icon = "fontawesome/brands/github" +#link = "https://github.com/user/repo" + +# ---------------------------------------------------------------------------- +# In this section you can configure the Markdown extensions that are used when +# rendering your documentation. We enable the most useful extensions by default, +# but you can customize this list to your needs. +# +# Read more: +# - https://zensical.org/docs/setup/extensions/ +# ---------------------------------------------------------------------------- +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.arithmatex] +generic = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.magiclink] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde] From 2d5a284156f20732d60c7df8b205d935ae07a1c3 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 15:59:46 +0200 Subject: [PATCH 03/16] ci: add Azure Static Web Apps workflow file on-behalf-of: @Azure opensource@microsoft.com --- ...-static-web-apps-purple-bush-012fe8010.yml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml diff --git a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml new file mode 100644 index 0000000..5c2cec7 --- /dev/null +++ b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml @@ -0,0 +1,46 @@ +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - feature/zensible + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - feature/zensible + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + lfs: false + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "/" # App source code path + api_location: "" # Api source code path - optional + output_location: "" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} + action: "close" From f063923f469973f347d56acb770de4f305ff0e36 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 16:01:41 +0200 Subject: [PATCH 04/16] Update CI for SWA --- ...-static-web-apps-purple-bush-012fe8010.yml | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml index 5c2cec7..78f595d 100644 --- a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml +++ b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml @@ -1,46 +1,53 @@ -name: Azure Static Web Apps CI/CD - -on: - push: - branches: - - feature/zensible - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - feature/zensible - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/" # App source code path - api_location: "" # Api source code path - optional - output_location: "" # Built app content directory - optional - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} - action: "close" +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - feature/zensible + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - feature/zensible + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + lfs: false + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: pip install zensical + - run: zensical build --clean + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "site" + api_location: "" + skip_app_build: true + skip_api_build: true + output_location: "" + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BUSH_012FE8010 }} + action: "close" From 1f6d3a652de8f90401db4ab8ac7989cf9b4c1e10 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 16:13:10 +0200 Subject: [PATCH 05/16] Move content from index.njk to index.md --- docs/index.md | 32 +++++++++++++++++++++++++++++ docs/index.njk | 56 -------------------------------------------------- 2 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 docs/index.njk diff --git a/docs/index.md b/docs/index.md index b50d7a1..80b4bc4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,38 @@ icon: lucide/rocket documentation still lives at https://telemetrydeck.com/docs/ for now. +--- + +## Welcome to TelemetryDeck + +TelemetryDeck helps you understand how users interact with your application through privacy-focused analytics. With just an SDK integration, you'll get valuable insights automatically – no complex configuration required. + +### Getting Started in 2 simple steps + + +- **Choose and install** the SDK for your platform from the guides below +- **Deploy your app** to start collecting data + +Once your updated app is in users' hands, TelemetryDeck will begin collecting data. It may take some time before you see meaningful insights, depending on your app's usage. + +### What's next after setup? + +After setting up the SDK and deploying your app, your most important next step is to learn how to use the dashboard to analyze your data: + +- [Analytics Walkthrough](/basics) - Learn how to navigate TelemetryDeck, interpret insights, and use analytics to make data-driven decisions that improve your app and grow your user base. + + +### Documentation Feedback + +If you find an error or feel like the documentation could be improved somewhat, we'd love +to hear from you! Either directly submit a change request with the buttons on each page, +or open an issue in our Docs GitHub Repository. + +The [documentation guide](/articles/documentation) explains all markdown and +additional features you can use while writing documentation for TelemetryDeck. + + + ---- # Get started diff --git a/docs/index.njk b/docs/index.njk deleted file mode 100644 index 398e42f..0000000 --- a/docs/index.njk +++ /dev/null @@ -1,56 +0,0 @@ ----json -{ -"title": "Quick Start Guide", -"lead": "Set up TelemetryDeck in minutes and start gaining insights into your app's performance", -"tags": "intro", -"categoryOrder": -9000, -"searchEngineDescription":"With this Quick Start Guide, you can set up TelemetryDeck's app analytics in minutes and start gaining insights into your app's performance." -} ---- - -

Welcome to TelemetryDeck

- -

TelemetryDeck helps you understand how users interact with your application through privacy-focused analytics. With just an SDK integration, you'll get valuable insights automatically – no complex configuration required.

- -

Getting Started in 2 simple steps

- -
    -
  1. Choose and install the SDK for your platform from the guides below
  2. -
  3. Deploy your app to start collecting data
  4. -
- -

Once your updated app is in users' hands, TelemetryDeck will begin collecting data. It may take some time before you see meaningful insights, depending on your app's usage.

- -{% include "docs/featured.njk" %} - -

What's next after setup?

- -

After setting up the SDK and deploying your app, your most important next step is to learn how to use the dashboard to analyze your data:

- -
-
-
- -
-

- - 📊 Analytics Walkthrough -

-

Learn how to navigate TelemetryDeck, interpret insights, and use analytics to make data-driven decisions that improve your app and grow your user base.

-

- Start here to get real value from your analytics - -

-
-
-
-
- -

Documentation Feedback

- -

If you find an error or feel like the documentation could be improved somewhat, we'd love -to hear from you! Either directly submit a change request with the buttons on each page, -or open an issue in our Docs GitHub Repository.

- -

The documentation guide explains all markdown and -additional features you can use while writing documentation for TelemetryDeck.

From f897f27f92da63df84b2da484bd8ec6830c0846e Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 16:15:40 +0200 Subject: [PATCH 06/16] Update Repo URL --- zensical.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zensical.toml b/zensical.toml index be1233c..c0743a8 100644 --- a/zensical.toml +++ b/zensical.toml @@ -8,6 +8,9 @@ [project] + +repo_url = "https://github.com/TelemetryDeck/docs" + # The site_name is shown in the page header and the browser window title # # Read more: https://zensical.org/docs/setup/basics/#site_name @@ -122,13 +125,13 @@ features = [ # will generate an edit button for the page. This works for common # repository hosting services. # https://zensical.org/docs/setup/repository/#content-actions - #"content.action.edit", + "content.action.edit", # If you have a repository configured and turn on this feature, Zensical # will generate a button that allows the user to view the Markdown # code for the current page. # https://zensical.org/docs/setup/repository/#content-actions - #"content.action.view", + "content.action.view", # Code annotations allow you to add an icon with a tooltip to your # code blocks to provide explanations at crucial points. From eb2ae3a07a83ffdc08948fbbb3864ce1645f5918 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 16:21:53 +0200 Subject: [PATCH 07/16] Rudimentary Color scheme --- zensical.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zensical.toml b/zensical.toml index c0743a8..3ffba6d 100644 --- a/zensical.toml +++ b/zensical.toml @@ -291,14 +291,25 @@ features = [ # - https://zensical.org/docs/setup/colors/ # ---------------------------------------------------------------------------- [[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" scheme = "default" toggle.icon = "lucide/sun" toggle.name = "Switch to dark mode" +primary = "deep orange" +accent = "orange" [[project.theme.palette]] +media = "(prefers-color-scheme: dark)" scheme = "slate" toggle.icon = "lucide/moon" -toggle.name = "Switch to light mode" +toggle.name = "Switch to system preference" +primary = "orange" +accent = "deep orange" # ---------------------------------------------------------------------------- # The "extra" section contains miscellaneous settings. From b9dececeb3c6095b5213c949ca7438058c7552db Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 17:19:35 +0200 Subject: [PATCH 08/16] Add TelemetryDeck App Analytics --- .../azure-static-web-apps-purple-bush-012fe8010.yml | 5 +++++ overrides/partials/integrations/analytics/telemetrydeck.html | 4 ++++ zensical.toml | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 overrides/partials/integrations/analytics/telemetrydeck.html diff --git a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml index 78f595d..593baec 100644 --- a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml +++ b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml @@ -19,6 +19,11 @@ jobs: with: submodules: true lfs: false + - name: Replace TelemetryDeck App ID + env: + TELEMETRYDECK_APP_ID: ${{ secrets.TELEMETRYDECK_APP_ID }} + run: | + sed -i "s|#TELEMETRYDECK_APP_ID#|${TELEMETRYDECK_APP_ID}|g" overrides/partials/integrations/analytics/telemetrydeck.html - uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/overrides/partials/integrations/analytics/telemetrydeck.html b/overrides/partials/integrations/analytics/telemetrydeck.html new file mode 100644 index 0000000..3ec627d --- /dev/null +++ b/overrides/partials/integrations/analytics/telemetrydeck.html @@ -0,0 +1,4 @@ + diff --git a/zensical.toml b/zensical.toml index 3ffba6d..01736c1 100644 --- a/zensical.toml +++ b/zensical.toml @@ -86,7 +86,7 @@ Copyright © 2026 TelemetryDeck GmbH # Read more: # - https://zensical.org/docs/customization/#extending-the-theme # -#custom_dir = "overrides" +custom_dir = "overrides" # With the "favicon" option you can set your own image to use as the icon # browsers will use in the browser title bar or tab bar. The path provided @@ -318,6 +318,9 @@ accent = "deep orange" #icon = "fontawesome/brands/github" #link = "https://github.com/user/repo" +[project.extra.analytics] +provider = "telemetrydeck" + # ---------------------------------------------------------------------------- # In this section you can configure the Markdown extensions that are used when # rendering your documentation. We enable the most useful extensions by default, From 82c9e077515a1682d661c8934cd19defa15d83cc Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 17:35:32 +0200 Subject: [PATCH 09/16] Convert warnings with regex --- docs/api/api-query-from-insight.md | 6 ++---- docs/api/api-run-query.md | 6 ++---- docs/api/api-token.md | 6 ++---- docs/guides/javascript-setup.md | 11 ++++------- docs/ingest/v1.md | 6 ++---- docs/integrations/revenuecat.md | 6 ++---- docs/tql/aggregators.md | 6 ++---- docs/tql/dimensionSpec.md | 6 ++---- docs/tql/funnels.md | 6 ++---- 9 files changed, 20 insertions(+), 39 deletions(-) diff --git a/docs/api/api-query-from-insight.md b/docs/api/api-query-from-insight.md index 543927d..36b58b0 100644 --- a/docs/api/api-query-from-insight.md +++ b/docs/api/api-query-from-insight.md @@ -6,11 +6,9 @@ description: Using the TelemetryDeck API, you can retrieve the query that is use lead: Using the TelemetryDeck API, you can retrieve the query that is used in an insight --- -{% notewarning "Paid plans only" %} +!!! warning "Paid plans only" -API access — including retrieving an insight's query — is available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and you'll be able to generate a personal access token and use the API. - -{% endnotewarning %} + API access — including retrieving an insight's query — is available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and you'll be able to generate a personal access token and use the API. ## Authorization diff --git a/docs/api/api-run-query.md b/docs/api/api-run-query.md index f7e6c04..c4d61de 100644 --- a/docs/api/api-run-query.md +++ b/docs/api/api-run-query.md @@ -7,11 +7,9 @@ lead: Using the TelemetryDeck API, you can run a query and retrieve its results order: 2 --- -{% notewarning "Paid plans only" %} +!!! warning "Paid plans only" -API access — including running queries — is available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and you'll be able to generate a personal access token and run queries. - -{% endnotewarning %} + API access — including running queries — is available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and you'll be able to generate a personal access token and run queries. ## Authorization diff --git a/docs/api/api-token.md b/docs/api/api-token.md index a13152c..c062aeb 100644 --- a/docs/api/api-token.md +++ b/docs/api/api-token.md @@ -7,11 +7,9 @@ lead: To interact with the TelemetryDeck API, you need a personal access token ( order: 1 --- -{% notewarning "Paid plans only" %} +!!! warning "Paid plans only" -Personal access tokens are available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and the feature will become available in the dashboard. - -{% endnotewarning %} + Personal access tokens are available on TelemetryDeck's paid plans. If your organization is on the free plan, upgrade your plan first and the feature will become available in the dashboard. ## Generating a Personal Access Token diff --git a/docs/guides/javascript-setup.md b/docs/guides/javascript-setup.md index e896f9b..cfa7060 100644 --- a/docs/guides/javascript-setup.md +++ b/docs/guides/javascript-setup.md @@ -87,14 +87,11 @@ If can't specify a user identifier at initialization, you can set it later by se Please note that `td.signal` is an async function that returns a promise. -{% notewarning "Special treatment for frameworks" %} +!!! warning "Special treatment for frameworks" -Some frameworks, like Svelte, don't need `crypto` and node.js. Here are some tips on how to implement TelemetryDeck when using some of these special frameworks: - -- The initialization should happen once, and the TD object should be passed around in a service or singleton. -- The `td.send` function should be used to send signals, either automatically in a router-like object or on a per-feature basis. - -{% endnotewarning %} + Some frameworks, like Svelte, don't need `crypto` and node.js. Here are some tips on how to implement TelemetryDeck when using some of these special frameworks: + - The initialization should happen once, and the TD object should be passed around in a service or singleton. + - The `td.send` function should be used to send signals, either automatically in a router-like object or on a per-feature basis. ### Advanced initialization options diff --git a/docs/ingest/v1.md b/docs/ingest/v1.md index 8a79b77..913b9c3 100644 --- a/docs/ingest/v1.md +++ b/docs/ingest/v1.md @@ -6,11 +6,9 @@ order: 99999 searchEngineDescription: A reference of all properties of the Signal Ingestion API. Note -- We’re continuing to host ingest v1, but officially deprecating it and recommending using v2. --- -{% notewarning "Deprecated" %} +!!! warning "Deprecated" -While we're continuing to host ingest v1, we're officially deprecating it and recommending our customers to use the [v2 API](/docs/ingest/v2/) instead. The v2 API is more flexible, has better support for custom data, and is more future proof. - -{% endnotewarning %} + While we're continuing to host ingest v1, we're officially deprecating it and recommending our customers to use the [v2 API](/docs/ingest/v2/) instead. The v2 API is more flexible, has better support for custom data, and is more future proof. This article will first show the minimal structure needed for a signal to send, and then talk about some of the optional extensions. diff --git a/docs/integrations/revenuecat.md b/docs/integrations/revenuecat.md index bf80a5a..427f6ed 100644 --- a/docs/integrations/revenuecat.md +++ b/docs/integrations/revenuecat.md @@ -84,11 +84,9 @@ Here's what's going on in the above example 2. We then manually set a default user for TelemetryDeck. This allows us to later retrieve a hashed version of the user identifier. 3. Finally, we configure RevenueCat and set up the necessary user attributes. -{% notewarning "You need to keep user identifiers in sync" %} +!!! warning "You need to keep user identifiers in sync" -Whenever you update your TelemetryDeck user identifier, you'll also need to update the user identifier in RevenueCat's `$telemetryDeckUserId` user attribute. - -{% endnotewarning %} + Whenever you update your TelemetryDeck user identifier, you'll also need to update the user identifier in RevenueCat's `$telemetryDeckUserId` user attribute. ## Setting up RevenueCat's TelemetryDeck Integration diff --git a/docs/tql/aggregators.md b/docs/tql/aggregators.md index fddc53d..818d02d 100644 --- a/docs/tql/aggregators.md +++ b/docs/tql/aggregators.md @@ -117,11 +117,9 @@ Also, you can use post aggregators to do union, intersection or difference on sk `cardinality` computes the cardinality of a dimension. -{% notewarning "Deprecated" %} +!!! warning "Deprecated" -This aggregator is deprecated. Use `thetaSketch` instead. - -{% endnotewarning %} + This aggregator is deprecated. Use `thetaSketch` instead. ```json { diff --git a/docs/tql/dimensionSpec.md b/docs/tql/dimensionSpec.md index 28cd830..1f8b1af 100644 --- a/docs/tql/dimensionSpec.md +++ b/docs/tql/dimensionSpec.md @@ -9,11 +9,9 @@ The default DimensionSpec returns dimension values as is and optionally renames If an extraction function is set, it returns dimension values transformed using the given extraction function. -{% notewarning "What is a dimension?" %} +!!! warning "What is a dimension?" -TelemetryDecks Query Language is based on Apache Druid. In Druid, a dimension is a named field in your data used for filtering, grouping, or joining - typically string or categorical values. - -{% endnotewarning %} + TelemetryDecks Query Language is based on Apache Druid. In Druid, a dimension is a named field in your data used for filtering, grouping, or joining - typically string or categorical values. ## Default DimensionSpec diff --git a/docs/tql/funnels.md b/docs/tql/funnels.md index 5b7d9b5..b92d15c 100644 --- a/docs/tql/funnels.md +++ b/docs/tql/funnels.md @@ -13,11 +13,9 @@ searchEngineDescription: This is a quick overview on how to create funnels, or c order: 1000 --- -{% notewarning "Deprecated" %} +!!! warning "Deprecated" -The content below is deprecated and will be removed in the future. Please refer to the [new documentation](/docs/tql/funnel/) instead. - -{% endnotewarning %} + The content below is deprecated and will be removed in the future. Please refer to the [new documentation](/docs/tql/funnel/) instead. This article explains the thought process on how to create funnel-type queries; scroll to the bottom for a complete example. From 99f7b35d60322cf2829b5bc75d8f7911fd23d78b Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 17:36:51 +0200 Subject: [PATCH 10/16] Convert infos with regex --- docs/articles/apple-app-privacy.md | 10 ++++------ docs/articles/documentation.md | 14 ++++++-------- docs/guides/javascript-setup.md | 16 +++++++--------- docs/guides/web-setup.md | 10 ++++------ docs/integrations/revenuecat.md | 14 +++++--------- docs/tql/aggregators.md | 8 +++----- docs/tql/funnels.md | 6 ++---- docs/tql/query.md | 8 +++----- 8 files changed, 34 insertions(+), 52 deletions(-) diff --git a/docs/articles/apple-app-privacy.md b/docs/articles/apple-app-privacy.md index 88c4cb1..3b4b959 100644 --- a/docs/articles/apple-app-privacy.md +++ b/docs/articles/apple-app-privacy.md @@ -34,14 +34,12 @@ In the second screen, scroll down until you see the **Identifiers** section. In TelemetryDeck's **default mode**, with no user identifier specified, check the **Device ID** checkmark. This is what identifies individual users to TelemetryDeck. -{% noteinfo "Other Types of Identifiers" %} +!!! warning "Other Types of Identifiers" -In case you use TelemetryDeck in a more advanced way where you supply a custom user identifier, you'll need to think about this for a second: + In case you use TelemetryDeck in a more advanced way where you supply a custom user identifier, you'll need to think about this for a second: -- If you instead specify a User Identifier such as email address or username to TelemetryDeck, check instead the **User ID** checkmark. The identifier is only transmitted as a hash, but it still counts as a user identifier. -- If you instead are purposefully disabling user tracking by handing the same string to TelemetryDeck for each user, you don't need to check any of the checkboxes in the **Identifiers** section. - -{% endnoteinfo %} + - If you instead specify a User Identifier such as email address or username to TelemetryDeck, check instead the **User ID** checkmark. The identifier is only transmitted as a hash, but it still counts as a user identifier. + - If you instead are purposefully disabling user tracking by handing the same string to TelemetryDeck for each user, you don't need to check any of the checkboxes in the **Identifiers** section. ### Usage Data diff --git a/docs/articles/documentation.md b/docs/articles/documentation.md index 8428599..ef8a8c0 100644 --- a/docs/articles/documentation.md +++ b/docs/articles/documentation.md @@ -79,16 +79,14 @@ Organizational tags like `docs` and `guides` are automatically applied. Use the - The experience level of the reader (`beginner`, `intermediate`, `advanced`) - The type of query (`filter`, `cohorts`, etc.) -{% noteinfo "You don't need to add 'docs' and 'articles' as tags" %} +!!! warning "You don't need to add 'docs' and 'articles' as tags" -All markdown files in the `docs` repository automatically get the `docs` tag applied to them (by `docs.11tydata.js`). In addition, the respective directories apply their own tags as well: + All markdown files in the `docs` repository automatically get the `docs` tag applied to them (by `docs.11tydata.js`). In addition, the respective directories apply their own tags as well: -- Files in the `intro` directory get the `intro` tag applied. -- Files in the `guides` directory get the `guides` tag applied. -- Files in the `articles` directory get the `articles` tag applied. -- Files in the `api` directory get the `api` tag applied. - -{% endnoteinfo %} + - Files in the `intro` directory get the `intro` tag applied. + - Files in the `guides` directory get the `guides` tag applied. + - Files in the `articles` directory get the `articles` tag applied. + - Files in the `api` directory get the `api` tag applied. ### Order diff --git a/docs/guides/javascript-setup.md b/docs/guides/javascript-setup.md index cfa7060..691a19a 100644 --- a/docs/guides/javascript-setup.md +++ b/docs/guides/javascript-setup.md @@ -14,17 +14,15 @@ order: 300 The TelemetryDeck SDK has no dependencies and supports **modern evergreen browsers** and **modern versions of Node.js** with support for [cryptography](https://caniuse.com/cryptography). -{% noteinfo "There are multiple ways of adding TelemetryDeck to a web site" %} +!!! warning "There are multiple ways of adding TelemetryDeck to a web site" -There are different tutorials you should read depending on your use case. + There are different tutorials you should read depending on your use case. -- The [TelemetryDeck Web SDK](/docs/guides/web-setup) is a quick and easy way to include web analytics into your website. This is fantastic for blogs, landing pages, static websites, and content-driven websites. - -- If you are building a JavaScript application – a Progressive Web App written in React, Vue, Angular, Svelte, Ember, or mobile or desktop apps written with React Native, Electron, Ionic, and so on, you should read this guide. - -[Our blog post](/blog/js-sdk-2-0/) explains the differences between the two SDKs in more detail. - -{% endnoteinfo %} + - The [TelemetryDeck Web SDK](/docs/guides/web-setup) is a quick and easy way to include web analytics into your website. This is fantastic for blogs, landing pages, static websites, and content-driven websites. + + - If you are building a JavaScript application – a Progressive Web App written in React, Vue, Angular, Svelte, Ember, or mobile or desktop apps written with React Native, Electron, Ionic, and so on, you should read this guide. + + [Our blog post](/blog/js-sdk-2-0/) explains the differences between the two SDKs in more detail. ## Set up diff --git a/docs/guides/web-setup.md b/docs/guides/web-setup.md index 0a97303..42ee7e9 100644 --- a/docs/guides/web-setup.md +++ b/docs/guides/web-setup.md @@ -109,14 +109,12 @@ This means that the Web SDK will recognize recurring users on the same day on th ## If you're a developer -{% noteinfo "There are multiple ways of adding TelemetryDeck to a web site" %} +!!! warning "There are multiple ways of adding TelemetryDeck to a web site" -There are different tutorials you should read depending on your use case. + There are different tutorials you should read depending on your use case. -- If you are building a **JavaScript application or PWA using node package manager**, you should read the [Node Package Setup Guide](/docs/guides/javascript-setup). -- If you are building a **website or blog**, and want to include TelemetryDeck with a simple script tag similar to Google Analytics or Plausible Analytics, you should read this guide. - -{% endnoteinfo %} + - If you are building a **JavaScript application or PWA using node package manager**, you should read the [Node Package Setup Guide](/docs/guides/javascript-setup). + - If you are building a **website or blog**, and want to include TelemetryDeck with a simple script tag similar to Google Analytics or Plausible Analytics, you should read this guide. ## What to do next diff --git a/docs/integrations/revenuecat.md b/docs/integrations/revenuecat.md index 427f6ed..8a4e73b 100644 --- a/docs/integrations/revenuecat.md +++ b/docs/integrations/revenuecat.md @@ -15,11 +15,9 @@ RevenueCat pairs excellently with TelemetryDeck – use TelemetryDeck to improv With this integration, you can import your RevenueCat events into TelemetryDeck, and see everything on one dashboard. We'll be using RevenueCat's _Webhooks_ feature to pass on their data to TelemetryDeck. -{% noteinfo "Read the announcement" %} +!!! warning "Read the announcement" -Our [announcement blog post](https://telemetrydeck.com/blog/revenuecat-integration/) shows you what to expect, how to use the new revenue dashboard, and gives examples on what to do with your revenue data. - -{% endnoteinfo %} + Our [announcement blog post](https://telemetrydeck.com/blog/revenuecat-integration/) shows you what to expect, how to use the new revenue dashboard, and gives examples on what to do with your revenue data. ## Installing RevenueCat and TelemetryDeck @@ -35,13 +33,11 @@ RevenueCat has a concept of **user attributes**. Our goal is to set two new user - `$telemetryDeckAppId`: This attribute should be set to your TelemetryDeck App ID, the same one you pass into the TelemetryDeck SDK for initialization. - `$telemetryDeckUserId`: This attribute needs to be the **already-hashed user identifier** that TelemetryDeck is using. -{% noteinfo "RevenueCat gets the hashed version of the TelemetryDeck User Identifier" %} - -While the TelemetryDeck SDK usually takes care of hashing for you, you'll need to extract the identifier after it's been hashed and pass that on to RevenueCat. Only then will the final identifiers in your TelemetryDeck dashboard match up. +!!! warning "RevenueCat gets the hashed version of the TelemetryDeck User Identifier" -If your version of the TelemetryDeck SDK does not expose a function to vend the hashed user identifier, you can hash it yourself using something like `SHA256(user_id + salt)`. + While the TelemetryDeck SDK usually takes care of hashing for you, you'll need to extract the identifier after it's been hashed and pass that on to RevenueCat. Only then will the final identifiers in your TelemetryDeck dashboard match up. -{% endnoteinfo %} + If your version of the TelemetryDeck SDK does not expose a function to vend the hashed user identifier, you can hash it yourself using something like `SHA256(user_id + salt)`. ### iOS diff --git a/docs/tql/aggregators.md b/docs/tql/aggregators.md index 818d02d..5be6e2b 100644 --- a/docs/tql/aggregators.md +++ b/docs/tql/aggregators.md @@ -97,13 +97,11 @@ A theta sketch gives you the number of unique values that a dimension has in you Theta sketches are a probabilistic data structure used for the [count-distinct problem](https://en.wikipedia.org/wiki/Count-distinct_problem). They allow us to quickly count elements in sets, such as the set of users in the aggregation buckets -{% noteinfo "More on ThetaSketches" %} +!!! warning "More on ThetaSketches" -A Theta sketch object can be thought of as a Set data structure. At query time, sketches are read and aggregated (set unioned) together. By default, you receive the estimate of the number of unique entries in the sketch object. + A Theta sketch object can be thought of as a Set data structure. At query time, sketches are read and aggregated (set unioned) together. By default, you receive the estimate of the number of unique entries in the sketch object. -Also, you can use post aggregators to do union, intersection or difference on sketch columns in the same row. This means you can create distinct sets of users and compare them against each other. This is necessary for retention and funnel queries. - -{% endnoteinfo %} + Also, you can use post aggregators to do union, intersection or difference on sketch columns in the same row. This means you can create distinct sets of users and compare them against each other. This is necessary for retention and funnel queries. ```json { diff --git a/docs/tql/funnels.md b/docs/tql/funnels.md index b92d15c..2aca4ad 100644 --- a/docs/tql/funnels.md +++ b/docs/tql/funnels.md @@ -112,11 +112,9 @@ For our filters, we want to grab all signals that might be relevant for the funn We're going to use aggregations to split up (or aggregate) the signals into different buckets, and count them by `clientUser` which is the field for TelemetryDeck's user identifier. We're using Theta Sketches to count the number of different users for the funnel stage. -{% noteinfo "What's a theta sketch?" %} +!!! warning "What's a theta sketch?" -A theta sketch is a probabilistic data structure used for the [count-distinct problem](https://en.wikipedia.org/wiki/Count-distinct_problem). It allows us to quickly count elements in sets, such as the set of users in the aggregation buckets. - -{% endnoteinfo %} + A theta sketch is a probabilistic data structure used for the [count-distinct problem](https://en.wikipedia.org/wiki/Count-distinct_problem). It allows us to quickly count elements in sets, such as the set of users in the aggregation buckets. ```json [ diff --git a/docs/tql/query.md b/docs/tql/query.md index 292ad82..2119a4e 100644 --- a/docs/tql/query.md +++ b/docs/tql/query.md @@ -11,13 +11,11 @@ Most queries for TelemetryDeck can be constructed using the regular Insight Edit You can use the TQL Query Builder to construct your query, or you can write it by hand. The minimum required properties are `granularity`, `dataSource` and `queryType`. The following sections describe the properties of the query object. -{% noteinfo "This looks curiously familiar" %} +!!! warning "This looks curiously familiar" -If you've ever worked with [Apache Druid](https://druid.apache.org), you'll notice a lot of similarities between TQL and Druid's native query language. This is because TQL is a superset of Druid's native query language, and TQL queries are compiled down into Druid queries before they're executed. + If you've ever worked with [Apache Druid](https://druid.apache.org), you'll notice a lot of similarities between TQL and Druid's native query language. This is because TQL is a superset of Druid's native query language, and TQL queries are compiled down into Druid queries before they're executed. -Druid queries can do various things that TQL queries can't do for security and privacy reasons, and Druid queries are also more verbose. To get into the fine details of Druid's query language, please check the [Apache Druid Documentation](https://druid.apache.org/docs/latest/querying/). - -{% endnoteinfo %} + Druid queries can do various things that TQL queries can't do for security and privacy reasons, and Druid queries are also more verbose. To get into the fine details of Druid's query language, please check the [Apache Druid Documentation](https://druid.apache.org/docs/latest/querying/). This example timeseries query returns the number of users per week: From 44173b82c54b6762ac2ae9c425ef701a1b4e2b12 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Mon, 4 May 2026 17:43:37 +0200 Subject: [PATCH 11/16] Update logo and favicon --- {images => docs/assets}/MAU_1.png | Bin {images => docs/assets}/MAU_2.png | Bin {images => docs/assets}/MAU_3.png | Bin {images => docs/assets}/MAU_4.png | Bin {images => docs/assets}/MAU_5.png | Bin .../assets}/Notebooks-Overview.png | Bin .../assets}/Notebooks-TQL-Live-Preview.png | Bin .../assets}/Peerigon_Logo_RGB_no_padding.svg | 0 {images => docs/assets}/Referrers_1.png | Bin {images => docs/assets}/Referrers_2.png | Bin {images => docs/assets}/Referrers_3a.png | Bin {images => docs/assets}/Referrers_3b.png | Bin {images => docs/assets}/Referrers_4.png | Bin {images => docs/assets}/Referrers_5.png | Bin .../assets}/Screenshot_SystemVersion.png | Bin {images => docs/assets}/aarrr-framework.png | Bin .../assets}/acquisition-active-vs-new.png | Bin .../acquisition-by-time-last-weeks.png | Bin .../assets}/acquisition-by-time.png | Bin .../acquisition-device-distribution.png | Bin .../acquisition-geographic-distribution.png | Bin .../assets}/acquisition-new-users.png | Bin .../assets}/activation-activated-users.png | Bin .../assets}/activation-by-time.png | Bin .../activation-device-distribution.png | Bin .../activation-geographic-distribution.png | Bin .../assets}/anonymization-display-image.jpg | Bin {images => docs/assets}/bar-chart.PNG | Bin {images => docs/assets}/compact_wide_mode.PNG | Bin .../assets}/create-new-insight.png | Bin .../assets}/daily-active-users-example.png | Bin .../assets}/dashboard-acquisition.png | Bin {images => docs/assets}/dashboard-custom.png | Bin .../assets}/dashboard-exploring-details.png | Bin .../assets}/dashboard-filtering-data.png | Bin .../assets}/dashboard-main-nav.png | Bin .../assets}/dashboard-notebooks.png | Bin .../assets}/dashboard-overview.png | Bin .../assets}/dashboard-signal-types.png | Bin {images => docs/assets}/display_mode.PNG | Bin {images => docs/assets}/donut-chart.PNG | Bin .../assets}/duration-signal-01.png | Bin .../assets}/duration-signal-02.png | Bin .../assets}/duration-signal-03.png | Bin .../assets}/edit-dashboard-name.png | Bin {images => docs/assets}/faq.jpg | Bin docs/assets/favicon.svg | 1 + {images => docs/assets}/filter_example.png | Bin {images => docs/assets}/filter_location.png | Bin {images => docs/assets}/funnel_type.PNG | Bin {images => docs/assets}/funnels_example.png | Bin .../assets}/funnels_set_filters.jpg | Bin {images => docs/assets}/line-chart.PNG | Bin .../assets}/location-create-dashboard.png | Bin .../assets}/location-edit-group.PNG | Bin .../assets}/location-edit-insight.png | Bin {images => docs/assets}/login-mask.png | Bin docs/assets/logo.svg | 31 ++++++++++++++++++ .../assets}/metrics-accessibility.png | Bin {images => docs/assets}/metrics-devices.png | Bin {images => docs/assets}/metrics-errors.png | Bin .../assets}/metrics-localization.png | Bin {images => docs/assets}/metrics-versions.png | Bin .../assets}/notebooks-video-thumbnail.png | Bin {images => docs/assets}/privacy-overview.png | Bin .../assets}/purchases-privacy-box.png | Bin {images => docs/assets}/rc-td-1.png | Bin {images => docs/assets}/rc-td-2.png | Bin {images => docs/assets}/retention-by-time.png | Bin .../assets}/retention-device-distribution.png | Bin .../retention-geographic-distribution.png | Bin .../assets}/retention-returning-users.png | Bin .../assets}/revenuecat_webhook_config.png | Bin .../assets}/system-versions-example.png | Bin {images => docs/assets}/table-chart.PNG | Bin {images => docs/assets}/test_mode.png | Bin {images => docs/assets}/typedigital.png | Bin .../assets}/update_next_major_version.png | Bin {images => docs/assets}/update_package.png | Bin .../assets}/xcode-swift-package.png | Bin .../assets}/xcode-swift-package1.png | Bin .../assets}/xcode-swift-package2.png | Bin {images => docs/assets}/yt-4-minute-setup.png | Bin {images => docs/assets}/yt-app-analytics.png | Bin .../assets}/yt-app-store-connect.png | Bin .../assets}/yt-built-in-charts.png | Bin {images => docs/assets}/yt-custom-charts.png | Bin zensical.toml | 4 +-- 88 files changed, 34 insertions(+), 2 deletions(-) rename {images => docs/assets}/MAU_1.png (100%) rename {images => docs/assets}/MAU_2.png (100%) rename {images => docs/assets}/MAU_3.png (100%) rename {images => docs/assets}/MAU_4.png (100%) rename {images => docs/assets}/MAU_5.png (100%) rename {images => docs/assets}/Notebooks-Overview.png (100%) rename {images => docs/assets}/Notebooks-TQL-Live-Preview.png (100%) rename {images => docs/assets}/Peerigon_Logo_RGB_no_padding.svg (100%) rename {images => docs/assets}/Referrers_1.png (100%) rename {images => docs/assets}/Referrers_2.png (100%) rename {images => docs/assets}/Referrers_3a.png (100%) rename {images => docs/assets}/Referrers_3b.png (100%) rename {images => docs/assets}/Referrers_4.png (100%) rename {images => docs/assets}/Referrers_5.png (100%) rename {images => docs/assets}/Screenshot_SystemVersion.png (100%) rename {images => docs/assets}/aarrr-framework.png (100%) rename {images => docs/assets}/acquisition-active-vs-new.png (100%) rename {images => docs/assets}/acquisition-by-time-last-weeks.png (100%) rename {images => docs/assets}/acquisition-by-time.png (100%) rename {images => docs/assets}/acquisition-device-distribution.png (100%) rename {images => docs/assets}/acquisition-geographic-distribution.png (100%) rename {images => docs/assets}/acquisition-new-users.png (100%) rename {images => docs/assets}/activation-activated-users.png (100%) rename {images => docs/assets}/activation-by-time.png (100%) rename {images => docs/assets}/activation-device-distribution.png (100%) rename {images => docs/assets}/activation-geographic-distribution.png (100%) rename {images => docs/assets}/anonymization-display-image.jpg (100%) rename {images => docs/assets}/bar-chart.PNG (100%) rename {images => docs/assets}/compact_wide_mode.PNG (100%) rename {images => docs/assets}/create-new-insight.png (100%) rename {images => docs/assets}/daily-active-users-example.png (100%) rename {images => docs/assets}/dashboard-acquisition.png (100%) rename {images => docs/assets}/dashboard-custom.png (100%) rename {images => docs/assets}/dashboard-exploring-details.png (100%) rename {images => docs/assets}/dashboard-filtering-data.png (100%) rename {images => docs/assets}/dashboard-main-nav.png (100%) rename {images => docs/assets}/dashboard-notebooks.png (100%) rename {images => docs/assets}/dashboard-overview.png (100%) rename {images => docs/assets}/dashboard-signal-types.png (100%) rename {images => docs/assets}/display_mode.PNG (100%) rename {images => docs/assets}/donut-chart.PNG (100%) rename {images => docs/assets}/duration-signal-01.png (100%) rename {images => docs/assets}/duration-signal-02.png (100%) rename {images => docs/assets}/duration-signal-03.png (100%) rename {images => docs/assets}/edit-dashboard-name.png (100%) rename {images => docs/assets}/faq.jpg (100%) create mode 100644 docs/assets/favicon.svg rename {images => docs/assets}/filter_example.png (100%) rename {images => docs/assets}/filter_location.png (100%) rename {images => docs/assets}/funnel_type.PNG (100%) rename {images => docs/assets}/funnels_example.png (100%) rename {images => docs/assets}/funnels_set_filters.jpg (100%) rename {images => docs/assets}/line-chart.PNG (100%) rename {images => docs/assets}/location-create-dashboard.png (100%) rename {images => docs/assets}/location-edit-group.PNG (100%) rename {images => docs/assets}/location-edit-insight.png (100%) rename {images => docs/assets}/login-mask.png (100%) create mode 100644 docs/assets/logo.svg rename {images => docs/assets}/metrics-accessibility.png (100%) rename {images => docs/assets}/metrics-devices.png (100%) rename {images => docs/assets}/metrics-errors.png (100%) rename {images => docs/assets}/metrics-localization.png (100%) rename {images => docs/assets}/metrics-versions.png (100%) rename {images => docs/assets}/notebooks-video-thumbnail.png (100%) rename {images => docs/assets}/privacy-overview.png (100%) rename {images => docs/assets}/purchases-privacy-box.png (100%) rename {images => docs/assets}/rc-td-1.png (100%) rename {images => docs/assets}/rc-td-2.png (100%) rename {images => docs/assets}/retention-by-time.png (100%) rename {images => docs/assets}/retention-device-distribution.png (100%) rename {images => docs/assets}/retention-geographic-distribution.png (100%) rename {images => docs/assets}/retention-returning-users.png (100%) rename {images => docs/assets}/revenuecat_webhook_config.png (100%) rename {images => docs/assets}/system-versions-example.png (100%) rename {images => docs/assets}/table-chart.PNG (100%) rename {images => docs/assets}/test_mode.png (100%) rename {images => docs/assets}/typedigital.png (100%) rename {images => docs/assets}/update_next_major_version.png (100%) rename {images => docs/assets}/update_package.png (100%) rename {images => docs/assets}/xcode-swift-package.png (100%) rename {images => docs/assets}/xcode-swift-package1.png (100%) rename {images => docs/assets}/xcode-swift-package2.png (100%) rename {images => docs/assets}/yt-4-minute-setup.png (100%) rename {images => docs/assets}/yt-app-analytics.png (100%) rename {images => docs/assets}/yt-app-store-connect.png (100%) rename {images => docs/assets}/yt-built-in-charts.png (100%) rename {images => docs/assets}/yt-custom-charts.png (100%) diff --git a/images/MAU_1.png b/docs/assets/MAU_1.png similarity index 100% rename from images/MAU_1.png rename to docs/assets/MAU_1.png diff --git a/images/MAU_2.png b/docs/assets/MAU_2.png similarity index 100% rename from images/MAU_2.png rename to docs/assets/MAU_2.png diff --git a/images/MAU_3.png b/docs/assets/MAU_3.png similarity index 100% rename from images/MAU_3.png rename to docs/assets/MAU_3.png diff --git a/images/MAU_4.png b/docs/assets/MAU_4.png similarity index 100% rename from images/MAU_4.png rename to docs/assets/MAU_4.png diff --git a/images/MAU_5.png b/docs/assets/MAU_5.png similarity index 100% rename from images/MAU_5.png rename to docs/assets/MAU_5.png diff --git a/images/Notebooks-Overview.png b/docs/assets/Notebooks-Overview.png similarity index 100% rename from images/Notebooks-Overview.png rename to docs/assets/Notebooks-Overview.png diff --git a/images/Notebooks-TQL-Live-Preview.png b/docs/assets/Notebooks-TQL-Live-Preview.png similarity index 100% rename from images/Notebooks-TQL-Live-Preview.png rename to docs/assets/Notebooks-TQL-Live-Preview.png diff --git a/images/Peerigon_Logo_RGB_no_padding.svg b/docs/assets/Peerigon_Logo_RGB_no_padding.svg similarity index 100% rename from images/Peerigon_Logo_RGB_no_padding.svg rename to docs/assets/Peerigon_Logo_RGB_no_padding.svg diff --git a/images/Referrers_1.png b/docs/assets/Referrers_1.png similarity index 100% rename from images/Referrers_1.png rename to docs/assets/Referrers_1.png diff --git a/images/Referrers_2.png b/docs/assets/Referrers_2.png similarity index 100% rename from images/Referrers_2.png rename to docs/assets/Referrers_2.png diff --git a/images/Referrers_3a.png b/docs/assets/Referrers_3a.png similarity index 100% rename from images/Referrers_3a.png rename to docs/assets/Referrers_3a.png diff --git a/images/Referrers_3b.png b/docs/assets/Referrers_3b.png similarity index 100% rename from images/Referrers_3b.png rename to docs/assets/Referrers_3b.png diff --git a/images/Referrers_4.png b/docs/assets/Referrers_4.png similarity index 100% rename from images/Referrers_4.png rename to docs/assets/Referrers_4.png diff --git a/images/Referrers_5.png b/docs/assets/Referrers_5.png similarity index 100% rename from images/Referrers_5.png rename to docs/assets/Referrers_5.png diff --git a/images/Screenshot_SystemVersion.png b/docs/assets/Screenshot_SystemVersion.png similarity index 100% rename from images/Screenshot_SystemVersion.png rename to docs/assets/Screenshot_SystemVersion.png diff --git a/images/aarrr-framework.png b/docs/assets/aarrr-framework.png similarity index 100% rename from images/aarrr-framework.png rename to docs/assets/aarrr-framework.png diff --git a/images/acquisition-active-vs-new.png b/docs/assets/acquisition-active-vs-new.png similarity index 100% rename from images/acquisition-active-vs-new.png rename to docs/assets/acquisition-active-vs-new.png diff --git a/images/acquisition-by-time-last-weeks.png b/docs/assets/acquisition-by-time-last-weeks.png similarity index 100% rename from images/acquisition-by-time-last-weeks.png rename to docs/assets/acquisition-by-time-last-weeks.png diff --git a/images/acquisition-by-time.png b/docs/assets/acquisition-by-time.png similarity index 100% rename from images/acquisition-by-time.png rename to docs/assets/acquisition-by-time.png diff --git a/images/acquisition-device-distribution.png b/docs/assets/acquisition-device-distribution.png similarity index 100% rename from images/acquisition-device-distribution.png rename to docs/assets/acquisition-device-distribution.png diff --git a/images/acquisition-geographic-distribution.png b/docs/assets/acquisition-geographic-distribution.png similarity index 100% rename from images/acquisition-geographic-distribution.png rename to docs/assets/acquisition-geographic-distribution.png diff --git a/images/acquisition-new-users.png b/docs/assets/acquisition-new-users.png similarity index 100% rename from images/acquisition-new-users.png rename to docs/assets/acquisition-new-users.png diff --git a/images/activation-activated-users.png b/docs/assets/activation-activated-users.png similarity index 100% rename from images/activation-activated-users.png rename to docs/assets/activation-activated-users.png diff --git a/images/activation-by-time.png b/docs/assets/activation-by-time.png similarity index 100% rename from images/activation-by-time.png rename to docs/assets/activation-by-time.png diff --git a/images/activation-device-distribution.png b/docs/assets/activation-device-distribution.png similarity index 100% rename from images/activation-device-distribution.png rename to docs/assets/activation-device-distribution.png diff --git a/images/activation-geographic-distribution.png b/docs/assets/activation-geographic-distribution.png similarity index 100% rename from images/activation-geographic-distribution.png rename to docs/assets/activation-geographic-distribution.png diff --git a/images/anonymization-display-image.jpg b/docs/assets/anonymization-display-image.jpg similarity index 100% rename from images/anonymization-display-image.jpg rename to docs/assets/anonymization-display-image.jpg diff --git a/images/bar-chart.PNG b/docs/assets/bar-chart.PNG similarity index 100% rename from images/bar-chart.PNG rename to docs/assets/bar-chart.PNG diff --git a/images/compact_wide_mode.PNG b/docs/assets/compact_wide_mode.PNG similarity index 100% rename from images/compact_wide_mode.PNG rename to docs/assets/compact_wide_mode.PNG diff --git a/images/create-new-insight.png b/docs/assets/create-new-insight.png similarity index 100% rename from images/create-new-insight.png rename to docs/assets/create-new-insight.png diff --git a/images/daily-active-users-example.png b/docs/assets/daily-active-users-example.png similarity index 100% rename from images/daily-active-users-example.png rename to docs/assets/daily-active-users-example.png diff --git a/images/dashboard-acquisition.png b/docs/assets/dashboard-acquisition.png similarity index 100% rename from images/dashboard-acquisition.png rename to docs/assets/dashboard-acquisition.png diff --git a/images/dashboard-custom.png b/docs/assets/dashboard-custom.png similarity index 100% rename from images/dashboard-custom.png rename to docs/assets/dashboard-custom.png diff --git a/images/dashboard-exploring-details.png b/docs/assets/dashboard-exploring-details.png similarity index 100% rename from images/dashboard-exploring-details.png rename to docs/assets/dashboard-exploring-details.png diff --git a/images/dashboard-filtering-data.png b/docs/assets/dashboard-filtering-data.png similarity index 100% rename from images/dashboard-filtering-data.png rename to docs/assets/dashboard-filtering-data.png diff --git a/images/dashboard-main-nav.png b/docs/assets/dashboard-main-nav.png similarity index 100% rename from images/dashboard-main-nav.png rename to docs/assets/dashboard-main-nav.png diff --git a/images/dashboard-notebooks.png b/docs/assets/dashboard-notebooks.png similarity index 100% rename from images/dashboard-notebooks.png rename to docs/assets/dashboard-notebooks.png diff --git a/images/dashboard-overview.png b/docs/assets/dashboard-overview.png similarity index 100% rename from images/dashboard-overview.png rename to docs/assets/dashboard-overview.png diff --git a/images/dashboard-signal-types.png b/docs/assets/dashboard-signal-types.png similarity index 100% rename from images/dashboard-signal-types.png rename to docs/assets/dashboard-signal-types.png diff --git a/images/display_mode.PNG b/docs/assets/display_mode.PNG similarity index 100% rename from images/display_mode.PNG rename to docs/assets/display_mode.PNG diff --git a/images/donut-chart.PNG b/docs/assets/donut-chart.PNG similarity index 100% rename from images/donut-chart.PNG rename to docs/assets/donut-chart.PNG diff --git a/images/duration-signal-01.png b/docs/assets/duration-signal-01.png similarity index 100% rename from images/duration-signal-01.png rename to docs/assets/duration-signal-01.png diff --git a/images/duration-signal-02.png b/docs/assets/duration-signal-02.png similarity index 100% rename from images/duration-signal-02.png rename to docs/assets/duration-signal-02.png diff --git a/images/duration-signal-03.png b/docs/assets/duration-signal-03.png similarity index 100% rename from images/duration-signal-03.png rename to docs/assets/duration-signal-03.png diff --git a/images/edit-dashboard-name.png b/docs/assets/edit-dashboard-name.png similarity index 100% rename from images/edit-dashboard-name.png rename to docs/assets/edit-dashboard-name.png diff --git a/images/faq.jpg b/docs/assets/faq.jpg similarity index 100% rename from images/faq.jpg rename to docs/assets/faq.jpg diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg new file mode 100644 index 0000000..dc4a92c --- /dev/null +++ b/docs/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/filter_example.png b/docs/assets/filter_example.png similarity index 100% rename from images/filter_example.png rename to docs/assets/filter_example.png diff --git a/images/filter_location.png b/docs/assets/filter_location.png similarity index 100% rename from images/filter_location.png rename to docs/assets/filter_location.png diff --git a/images/funnel_type.PNG b/docs/assets/funnel_type.PNG similarity index 100% rename from images/funnel_type.PNG rename to docs/assets/funnel_type.PNG diff --git a/images/funnels_example.png b/docs/assets/funnels_example.png similarity index 100% rename from images/funnels_example.png rename to docs/assets/funnels_example.png diff --git a/images/funnels_set_filters.jpg b/docs/assets/funnels_set_filters.jpg similarity index 100% rename from images/funnels_set_filters.jpg rename to docs/assets/funnels_set_filters.jpg diff --git a/images/line-chart.PNG b/docs/assets/line-chart.PNG similarity index 100% rename from images/line-chart.PNG rename to docs/assets/line-chart.PNG diff --git a/images/location-create-dashboard.png b/docs/assets/location-create-dashboard.png similarity index 100% rename from images/location-create-dashboard.png rename to docs/assets/location-create-dashboard.png diff --git a/images/location-edit-group.PNG b/docs/assets/location-edit-group.PNG similarity index 100% rename from images/location-edit-group.PNG rename to docs/assets/location-edit-group.PNG diff --git a/images/location-edit-insight.png b/docs/assets/location-edit-insight.png similarity index 100% rename from images/location-edit-insight.png rename to docs/assets/location-edit-insight.png diff --git a/images/login-mask.png b/docs/assets/login-mask.png similarity index 100% rename from images/login-mask.png rename to docs/assets/login-mask.png diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..ae16198 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/metrics-accessibility.png b/docs/assets/metrics-accessibility.png similarity index 100% rename from images/metrics-accessibility.png rename to docs/assets/metrics-accessibility.png diff --git a/images/metrics-devices.png b/docs/assets/metrics-devices.png similarity index 100% rename from images/metrics-devices.png rename to docs/assets/metrics-devices.png diff --git a/images/metrics-errors.png b/docs/assets/metrics-errors.png similarity index 100% rename from images/metrics-errors.png rename to docs/assets/metrics-errors.png diff --git a/images/metrics-localization.png b/docs/assets/metrics-localization.png similarity index 100% rename from images/metrics-localization.png rename to docs/assets/metrics-localization.png diff --git a/images/metrics-versions.png b/docs/assets/metrics-versions.png similarity index 100% rename from images/metrics-versions.png rename to docs/assets/metrics-versions.png diff --git a/images/notebooks-video-thumbnail.png b/docs/assets/notebooks-video-thumbnail.png similarity index 100% rename from images/notebooks-video-thumbnail.png rename to docs/assets/notebooks-video-thumbnail.png diff --git a/images/privacy-overview.png b/docs/assets/privacy-overview.png similarity index 100% rename from images/privacy-overview.png rename to docs/assets/privacy-overview.png diff --git a/images/purchases-privacy-box.png b/docs/assets/purchases-privacy-box.png similarity index 100% rename from images/purchases-privacy-box.png rename to docs/assets/purchases-privacy-box.png diff --git a/images/rc-td-1.png b/docs/assets/rc-td-1.png similarity index 100% rename from images/rc-td-1.png rename to docs/assets/rc-td-1.png diff --git a/images/rc-td-2.png b/docs/assets/rc-td-2.png similarity index 100% rename from images/rc-td-2.png rename to docs/assets/rc-td-2.png diff --git a/images/retention-by-time.png b/docs/assets/retention-by-time.png similarity index 100% rename from images/retention-by-time.png rename to docs/assets/retention-by-time.png diff --git a/images/retention-device-distribution.png b/docs/assets/retention-device-distribution.png similarity index 100% rename from images/retention-device-distribution.png rename to docs/assets/retention-device-distribution.png diff --git a/images/retention-geographic-distribution.png b/docs/assets/retention-geographic-distribution.png similarity index 100% rename from images/retention-geographic-distribution.png rename to docs/assets/retention-geographic-distribution.png diff --git a/images/retention-returning-users.png b/docs/assets/retention-returning-users.png similarity index 100% rename from images/retention-returning-users.png rename to docs/assets/retention-returning-users.png diff --git a/images/revenuecat_webhook_config.png b/docs/assets/revenuecat_webhook_config.png similarity index 100% rename from images/revenuecat_webhook_config.png rename to docs/assets/revenuecat_webhook_config.png diff --git a/images/system-versions-example.png b/docs/assets/system-versions-example.png similarity index 100% rename from images/system-versions-example.png rename to docs/assets/system-versions-example.png diff --git a/images/table-chart.PNG b/docs/assets/table-chart.PNG similarity index 100% rename from images/table-chart.PNG rename to docs/assets/table-chart.PNG diff --git a/images/test_mode.png b/docs/assets/test_mode.png similarity index 100% rename from images/test_mode.png rename to docs/assets/test_mode.png diff --git a/images/typedigital.png b/docs/assets/typedigital.png similarity index 100% rename from images/typedigital.png rename to docs/assets/typedigital.png diff --git a/images/update_next_major_version.png b/docs/assets/update_next_major_version.png similarity index 100% rename from images/update_next_major_version.png rename to docs/assets/update_next_major_version.png diff --git a/images/update_package.png b/docs/assets/update_package.png similarity index 100% rename from images/update_package.png rename to docs/assets/update_package.png diff --git a/images/xcode-swift-package.png b/docs/assets/xcode-swift-package.png similarity index 100% rename from images/xcode-swift-package.png rename to docs/assets/xcode-swift-package.png diff --git a/images/xcode-swift-package1.png b/docs/assets/xcode-swift-package1.png similarity index 100% rename from images/xcode-swift-package1.png rename to docs/assets/xcode-swift-package1.png diff --git a/images/xcode-swift-package2.png b/docs/assets/xcode-swift-package2.png similarity index 100% rename from images/xcode-swift-package2.png rename to docs/assets/xcode-swift-package2.png diff --git a/images/yt-4-minute-setup.png b/docs/assets/yt-4-minute-setup.png similarity index 100% rename from images/yt-4-minute-setup.png rename to docs/assets/yt-4-minute-setup.png diff --git a/images/yt-app-analytics.png b/docs/assets/yt-app-analytics.png similarity index 100% rename from images/yt-app-analytics.png rename to docs/assets/yt-app-analytics.png diff --git a/images/yt-app-store-connect.png b/docs/assets/yt-app-store-connect.png similarity index 100% rename from images/yt-app-store-connect.png rename to docs/assets/yt-app-store-connect.png diff --git a/images/yt-built-in-charts.png b/docs/assets/yt-built-in-charts.png similarity index 100% rename from images/yt-built-in-charts.png rename to docs/assets/yt-built-in-charts.png diff --git a/images/yt-custom-charts.png b/docs/assets/yt-custom-charts.png similarity index 100% rename from images/yt-custom-charts.png rename to docs/assets/yt-custom-charts.png diff --git a/zensical.toml b/zensical.toml index 01736c1..22920b5 100644 --- a/zensical.toml +++ b/zensical.toml @@ -96,7 +96,7 @@ custom_dir = "overrides" # - https://zensical.org/docs/setup/logo-and-icons/#favicon # - https://developer.mozilla.org/en-US/docs/Glossary/Favicon # -#favicon = "images/favicon.png" +favicon = "assets/favicon.png" # Zensical supports more than 60 different languages. This means that the # labels and tooltips that Zensical's templates produce are translated. @@ -257,7 +257,7 @@ features = [ # option in the "theme" subsection. The logo must be a relative path to a file # in your "docs_dir", e.g., to use `docs/assets/logo.png` you would set: # ---------------------------------------------------------------------------- -#logo = "assets/logo.png" +logo = "assets/logo.svg" # ---------------------------------------------------------------------------- # If you don't have a dedicated project logo, you can use a built-in icon from From 0743d6545153624c3445c25d3d38795027a8b66b Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Sun, 10 May 2026 11:57:09 +0200 Subject: [PATCH 12/16] docs: update README with local dev instructions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 292768f..4ca93c3 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,10 @@ Welcome to the public documentation for TelemetryDeck. The output of these pages We're incredibly grateful for pull requests or issues that improve things or let us know about errata and missing or unclear bits of information! <3 Check out `articles/documentation.md` for an overview of how documentation pages are formatted. + + +## Local Development + +We use Python `pip`. You can follow the setups at https://zensical.org/docs/get-started/#installation for the initial setup. + +Note: Make sure the Python version is recent e.g. 3.14 or later (`python3 --version`). From 53eac2d68fa9fce2f3aea8423517712a7daedd80 Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Sun, 10 May 2026 12:16:56 +0200 Subject: [PATCH 13/16] feat: fix image references --- docs/articles/anonymization-how-it-works.md | 6 +++--- docs/articles/apple-app-privacy.md | 2 +- docs/articles/create-custom-dashboards.md | 8 ++++---- docs/articles/decide-to-drop-ios-version.md | 2 +- docs/articles/documentation.md | 14 +++++++------- docs/articles/duration-signals.md | 6 +++--- docs/articles/how-to-funnel-insights.md | 6 +++--- docs/articles/insights-about-daily-users.md | 2 +- docs/articles/insights-about-monthly-users.md | 10 +++++----- docs/articles/insights-about-referrers.md | 12 ++++++------ docs/articles/insights-about-system-version.md | 2 +- docs/articles/insights.md | 18 +++++++++--------- docs/articles/login-options.md | 2 +- docs/articles/notebooks.md | 6 +++--- docs/articles/preset-purchases.md | 2 +- docs/articles/set-up-filters-insights.md | 4 ++-- docs/articles/test-mode.md | 2 +- docs/articles/update-package.md | 4 ++-- docs/basics/acquisition.md | 12 ++++++------ docs/basics/activation.md | 8 ++++---- docs/basics/index.md | 18 +++++++++--------- docs/basics/metrics.md | 10 +++++----- docs/basics/pirate-metrics.md | 2 +- docs/basics/retention.md | 8 ++++---- docs/guides/objective-c-setup.md | 2 +- docs/guides/privacy-faq.md | 2 +- docs/guides/react-setup.md | 2 +- docs/guides/swift-setup.md | 6 +++--- docs/guides/vue-setup.md | 2 +- docs/integrations/revenuecat.md | 4 ++-- 30 files changed, 92 insertions(+), 92 deletions(-) diff --git a/docs/articles/anonymization-how-it-works.md b/docs/articles/anonymization-how-it-works.md index 5f74634..ab589f2 100644 --- a/docs/articles/anonymization-how-it-works.md +++ b/docs/articles/anonymization-how-it-works.md @@ -8,7 +8,7 @@ description: TelemetryDeck anonymizes user data, by double salting and hashing I lead: We take user privacy seriously. To ensure the privacy of our users, we use various techniques to anonymize user data. One of the ways we do this is by using a double hashing technique to anonymize user identifiers. searchEngineTitle: How TelemetryDeck anonymizes user data privacy-friendly searchEngineDescription: TelemetryDeck anonymizes user data, by double hashing and salting IDs, to ensure anonymity and protect user privacy while still providing valuable insights. -headerImage: /docs/images/anonymization-display-image.jpg +headerImage: /assets/anonymization-display-image.jpg --- ## Methodology @@ -19,7 +19,7 @@ At TelemetryDeck, we even go a step further: the “identification date” is no **Check out why [App Store Connect is not enough](https://www.youtube.com/watch?v=4_WqJGPzgmY) to protect user data!** -[![Compare TelemetryDeck and App Store Connect for Analytics](/docs/images/yt-app-store-connect.png)](https://www.youtube.com/watch?v=4_WqJGPzgmY) +[![Compare TelemetryDeck and App Store Connect for Analytics](/assets/yt-app-store-connect.png)](https://www.youtube.com/watch?v=4_WqJGPzgmY) ## User identifiers @@ -35,7 +35,7 @@ If you use the same (custom) salt and provide the same original identifier, you **Check out our [YouTube Channel](https://www.youtube.com/watch?v=4sQcZoi21nU) for a deeper dive into our anonymization process!** -[![The Benefits of TelemetryDeck For Privacy Focused App Analytics](/docs/images/yt-app-analytics.png)](https://www.youtube.com/watch?v=4sQcZoi21nU) +[![The Benefits of TelemetryDeck For Privacy Focused App Analytics](/assets/yt-app-analytics.png)](https://www.youtube.com/watch?v=4sQcZoi21nU) ## Conclusion diff --git a/docs/articles/apple-app-privacy.md b/docs/articles/apple-app-privacy.md index 3b4b959..267bfa3 100644 --- a/docs/articles/apple-app-privacy.md +++ b/docs/articles/apple-app-privacy.md @@ -16,7 +16,7 @@ Since TelemetryDeck's analytics are private by default, you can include this inf This means your app will get an excellent privacy rating. -![A screenshot of Apple's Privacy Overview](/docs/images/privacy-overview.png) +![A screenshot of Apple's Privacy Overview](/assets/privacy-overview.png) ## How do I start? diff --git a/docs/articles/create-custom-dashboards.md b/docs/articles/create-custom-dashboards.md index 0aadfae..8216dc8 100644 --- a/docs/articles/create-custom-dashboards.md +++ b/docs/articles/create-custom-dashboards.md @@ -16,7 +16,7 @@ order: To create a dashboard for your insights, go to your [TelemetryDeck dashboard](https://dashboard.telemetrydeck.com/), navigate to `Dashboards` and look at the left sidebar. Here you can find the `New Dashboard` button. Create a _New Dashboard_, give your dashboard a name and then click _Create_. You now have a new insight dashboard in your sidebar! -![Location of the "Create New Dashboard" Button](/docs/images/location-create-dashboard.png) +![Location of the "Create New Dashboard" Button](/assets/location-create-dashboard.png) ## Share your dashboards with your organization @@ -31,20 +31,20 @@ You can also share specific dashboards with the members of your organization. To Now let's **create** an insight. While in your newly created dashboard, navigate to the `Actions` drop down button on the top-right side. Click the _Create new Insight_ button and give your new insight a name. Please choose from one of our convenient templates! The insight will be automagically ✨ generated for you. If needed, you can also change the display mode, title, query type, and more - even after creating it. -![Location of the "Create New Insight" Button](/docs/images/create-new-insight.png) +![Location of the "Create New Insight" Button](/assets/create-new-insight.png) You can create a bunch of insights inside your dashboards and thus, combine thematically related charts of your insights. And you're able to resize your charts to regular-sized `Compact` cards or big `Wide` cards. To **delete** insights from a dashboard, click on the insights' name. Go to `Actions` in the top right corner, just delete the current insight by clicking on "Delete Insight". -![Location of the "Delete Insight" Button](/docs/images/location-edit-insight.png) +![Location of the "Delete Insight" Button](/assets/location-edit-insight.png) ### Rename your dashboard Not happy with the name given to your dashboard? No problem - you can change it quickly! Click on the dashboard whose name you want to change. Then hover over your dashboard name, and click to edit. Save your changes after you are done editing - your dashboard now has a new name. -![Location of the "Rename Dashboard" Button](/docs/images/edit-dashboard-name.png) +![Location of the "Rename Dashboard" Button](/assets/edit-dashboard-name.png) ### Delete your dashboard diff --git a/docs/articles/decide-to-drop-ios-version.md b/docs/articles/decide-to-drop-ios-version.md index ddd9c7b..6380978 100644 --- a/docs/articles/decide-to-drop-ios-version.md +++ b/docs/articles/decide-to-drop-ios-version.md @@ -27,7 +27,7 @@ The TelemetryDeck SDK by default tries to collect data about the user's operatin To find out which percentage of your users are using a certain OS version, use a **Breakdown Insight** (also known as a **Top N Insight**). Then you'll have multiple options for the breakdown key. TelemetryDeck provides pre-built dashboards, among others the Top N Insight just described: -![Screenshot SystemVersion](/docs/images/Screenshot_SystemVersion.png) +![Screenshot SystemVersion](/assets/Screenshot_SystemVersion.png) You can switch between `majorMinorVersion` and `majorVersion` in the chart. Below we come back to the difference with recommendations what to use for this purpose. As an orientation beyond your app we also want to point out our monthly updated [surveys](https://telemetrydeck.com/survey/). Through these we provide information about [majorMinorVersions](https://telemetrydeck.com/survey/apple/iOS/minorSystemVersions/) and [majorVersions](https://telemetrydeck.com/survey/apple/iOS/majorSystemVersions/) currently used according to our data set as well as device related information regarding [iPhone models](https://telemetrydeck.com/survey/apple/iPhone/models/) for example. diff --git a/docs/articles/documentation.md b/docs/articles/documentation.md index ef8a8c0..8c841da 100644 --- a/docs/articles/documentation.md +++ b/docs/articles/documentation.md @@ -186,25 +186,25 @@ and [links](https://www.markdownguide.org/basic-syntax/#link). ## Images -To display an image in a docs article, add it to the `images` directory. You can then link it using regular markdown image syntax, adding `/docs/images/` before the images' name. +To display an image in a docs article, add it to the `assets` directory. You can then link it using regular markdown image syntax, adding `/assets/` before the image's name. -Example: You just added the file `privacy-overview.png` to the `images` folder. You can now display that image like so: +Example: You just added the file `privacy-overview.png` to the `assets` folder. You can now display that image like so: ```markdown -![A screenshot of Apple's Privacy Overview](/docs/images/privacy-overview.png) +![A screenshot of Apple's Privacy Overview](/assets/privacy-overview.png) ``` -The first part is the image's alt text. The second part is the path (`/docs/images/`) and the image file name `privacy-overview.png`. +The first part is the image's alt text. The second part is the path (`/assets/`) and the image file name `privacy-overview.png`. Here's what it looks like: -![A screenshot of Apple's Privacy Overview](/docs/images/privacy-overview.png) +![A screenshot of Apple's Privacy Overview](/assets/privacy-overview.png) {% notewarning "Image File Locations" %} -Image files need to live in the `images` directory at the root of the `docs` repository. Image files that are elsewhere in the file hierarchy will be ignored. +Image files need to live in the `assets` directory inside `docs/`. Image files elsewhere in the file hierarchy will be ignored. -You need to prefix `/docs/images/` to the path when linking to the image, since the Docs repository lives inside a `/docs` folder on deployment. +Prefix `/assets/` to the path when linking to the image — the path is resolved against the docs site root. {% endnotewarning %} ## Code Blocks diff --git a/docs/articles/duration-signals.md b/docs/articles/duration-signals.md index 3cc9440..6ec2bbf 100644 --- a/docs/articles/duration-signals.md +++ b/docs/articles/duration-signals.md @@ -84,11 +84,11 @@ The histogram aggregation type is perfect for visualizing the distribution of du 1. Create a new insight of type "Advanced Query", then open the "JSON Editor": - ![A screenshot of the Query Creator dialog](/docs/images/duration-signal-01.png) + ![A screenshot of the Query Creator dialog](/assets/duration-signal-01.png) 2. Copy & paste the following histogram aggregation query and adjust `` to your needs: - ![A screenshot of the JSON Editor text field](/docs/images/duration-signal-02.png) + ![A screenshot of the JSON Editor text field](/assets/duration-signal-02.png) ```json { @@ -125,7 +125,7 @@ The histogram aggregation type is perfect for visualizing the distribution of du 3. Set the chart type to be a bar chart in the UI via the insight's top right segmented control: - ![A screenshot of the insight set to be a bar chart](/docs/images/duration-signal-03.png) + ![A screenshot of the insight set to be a bar chart](/assets/duration-signal-03.png) 4. You might also want to adjust the `splitPoints` array based on the expected duration of your activity, for example: - **Short interactions** (button clicks): `[0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1, 2, 5]` diff --git a/docs/articles/how-to-funnel-insights.md b/docs/articles/how-to-funnel-insights.md index 236a704..8a15ccd 100644 --- a/docs/articles/how-to-funnel-insights.md +++ b/docs/articles/how-to-funnel-insights.md @@ -16,9 +16,9 @@ Sometimes they go directly to their goal, do not pass Go, and do not collect 200 These goals are often your goals, too. As the app owner, you want them to spend time in your app, find the new feature, and order that new product. -![Screenshot of a funnel called "Log in Journey" with 4 decreasing steps, going from "user login", to "show group view", to "show insight", and finally to "widget setup"](/docs/images/funnels_example.png) +![Screenshot of a funnel called "Log in Journey" with 4 decreasing steps, going from "user login", to "show group view", to "show insight", and finally to "widget setup"](/assets/funnels_example.png) -![Screenshot of the funnel steps with set filters for "Log in Journey"](/docs/images/funnels_set_filters.jpg) +![Screenshot of the funnel steps with set filters for "Log in Journey"](/assets/funnels_set_filters.jpg) Funnels are a great way to show this kind of behavior. What funnels do at heart is display **when** a user **ends the process**. For example, it is possible to showcase each page of an app - from opening the home page to the final checkout page. If you have 100 users going through that process, and all of them finish it, all steps should show 100 users. If there happens to be some confusion during the process, and users cancel it before the checkout, you will see that the page causing that issue will leave a drop in users afterward. The checkout page will probably show fewer than 100 users at the end. @@ -28,7 +28,7 @@ Let's have a look at how to use funnels in TelemetryDeck. ## How to use funnels Imagine you want to track how many users turn into customers. You can use funnels to track this conversion process. Since a funnel filters your insights by conditions, you must set up filters in your TelemetryDeck dashboard with the **filter editor**. These filters help you track how many users complete each funnel step, leading to a great visualization of your app's conversion. To create a **funnel chart**, just set the chart type to funnel and the query type to **funnel query** - or use our handy template to create an empty funnel! -![Screenshot of the selected funnel query and chart type](/docs/images/funnel_type.PNG) +![Screenshot of the selected funnel query and chart type](/assets/funnel_type.PNG) For example, let's say your funnel has three steps: diff --git a/docs/articles/insights-about-daily-users.md b/docs/articles/insights-about-daily-users.md index c777a80..c55a4ea 100644 --- a/docs/articles/insights-about-daily-users.md +++ b/docs/articles/insights-about-daily-users.md @@ -19,7 +19,7 @@ Insights of daily active users are the best way to see how your app is doing. Yo We will help you set up your insight so you can track your users right away, privacy-friendly and anonymized! -![Bar chart example of daily active users](/docs/images/daily-active-users-example.png) +![Bar chart example of daily active users](/assets/daily-active-users-example.png) ## Navigate to your app diff --git a/docs/articles/insights-about-monthly-users.md b/docs/articles/insights-about-monthly-users.md index 2e6bcff..94597de 100644 --- a/docs/articles/insights-about-monthly-users.md +++ b/docs/articles/insights-about-monthly-users.md @@ -18,21 +18,21 @@ order: Follow these steps to get an insight of your monthly active users (MAU) within a couple of minutes: 1. Go to the dashboard and create a New Insight. - ![Create a new insight](/docs/images/MAU_1.png) + ![Create a new insight](/assets/MAU_1.png) 2. In the next step select "I don’t know, show me some examples" and then choose "Monthly Users". 3. Now you are already getting results! Unfortunately, you probably still do not find the chart that helpful since only data of the last 30 days is included and that contradicts a chart with granularity set to "Month". - ![Standard insight](/docs/images/MAU_2.png) + ![Standard insight](/assets/MAU_2.png) **A quick fix** is changing the included data from "Last 30 Days" to "This Year" in the respective dropdown at the top of the page. - ![Insight current year](/docs/images/MAU_3.png) + ![Insight current year](/assets/MAU_3.png) 4. An issue is that you do not want to constantly change the time interval manually every time you use TelemetryDeck for this query to work. To resolve this: - While editing the query click on the **Query Type** button and choose **Advanced Query**. This will cause that you stop editing the query, but once you click on "Edit" that same query again you can now use the Visual Editor. - In the Metadata section add an item for Relative Intervals - At beginningDate choose `month`, `-12` and `beginning` to start the time interval 12 months ago and for **endDate** choose `month`, `0` and `end` to end the time interval at the current time. - ![Set relative interval](/docs/images/MAU_4.png) + ![Set relative interval](/assets/MAU_4.png) The query is now working as required although the dropdown at the top is changed back to "Last 30 Days". -![Final insight](/docs/images/MAU_5.png) +![Final insight](/assets/MAU_5.png) diff --git a/docs/articles/insights-about-referrers.md b/docs/articles/insights-about-referrers.md index f922729..30eb3f6 100644 --- a/docs/articles/insights-about-referrers.md +++ b/docs/articles/insights-about-referrers.md @@ -16,10 +16,10 @@ order: --- To begin with, add a new insight in the dashboard of your choice and select **Advanced Query** so that you can proceed using the **Visual Editor**. -![Create a new advanced query](/docs/images/Referrers_1.png) +![Create a new advanced query](/assets/Referrers_1.png) 1. In the first section of the Visual Editor, **Metadata**, choose a `topN query` - we propose granularity `all` so that you can display the information in a donut chart in the end. - ![Metadata](/docs/images/Referrers_2.png) + ![Metadata](/assets/Referrers_2.png) 2. In the next step you can choose which **Data to Include**. To get started, add `thisApp` as well as the respective App ID. @@ -27,15 +27,15 @@ To begin with, add a new insight in the dashboard of your choice and select **Ad To achieve that add a `regex` filter with dimension `referrer` and domain as well as top-level-domain of your website separated by a `\`: `referrer~yourDomain\.com`. Please be aware that this is a rather basic regex filter as the purpose here is to show the creation of the query. More elements can be added to the filter of course. Once you have entered the information for the filter click on the field again and select `Negate (Wrap in "not" condition)` - ![Data to Include_1](/docs/images/Referrers_3a.png) + ![Data to Include_1](/assets/Referrers_3a.png) When you are finished, the filter in **Data to Include** section should look as follows: - ![Data to Include_2](/docs/images/Referrers_3b.png) + ![Data to Include_2](/assets/Referrers_3b.png) Additionally, feel free to add further information in the regex filter in case you want to exclude search engines besides your own website as referrers for example. This could be used in the `regex` filter of the Visual Editor to expand the filter: `(yourDomain\.com|google\.|bing\.|search\.brave|yandex\.|kagi\.|duckduckgo\.)`. 4. The following section are **topN Options**. In the first part regarding the dimension, select type `default` and type in `referrer` two times. Furthermore, choose a `numeric` metric and add `count`. To wrap this section up choose the number of topN results to be returned, i.e. the threshold. - ![topN Options](/docs/images/Referrers_4.png) + ![topN Options](/assets/Referrers_4.png) 5. Finally, add a `longSum`-aggregator for the **Aggregations** and type in `count` in the two respective fields. - ![Aggregations](/docs/images/Referrers_5.png) + ![Aggregations](/assets/Referrers_5.png) To wrap the creation of the insight up we recommend using a `Donut Chart` to display topN of the query results. diff --git a/docs/articles/insights-about-system-version.md b/docs/articles/insights-about-system-version.md index 038fd8e..a826808 100644 --- a/docs/articles/insights-about-system-version.md +++ b/docs/articles/insights-about-system-version.md @@ -34,7 +34,7 @@ Click on _+ Create new Insight_ and give it a name. Now you can either build you You can directly see how many users are using your app from a specific operating system. By default, the versions are grouped by minor versions (displayed as `15.x.x`). -![Donut chart example of system versions](/docs/images/system-versions-example.png) +![Donut chart example of system versions](/assets/system-versions-example.png) ## Step four: Configure your insights diff --git a/docs/articles/insights.md b/docs/articles/insights.md index 4fc8c23..53e7fe6 100644 --- a/docs/articles/insights.md +++ b/docs/articles/insights.md @@ -21,7 +21,7 @@ The query is used to retrieve data from the TelemetryDeck API, and the display m **Check out our [YouTube Channel](https://www.youtube.com/watch?v=x0eZoQDnAPA) to learn how our built-in charts work!** -[![Understanding TelemetryDeck's built-in charts](/docs/images/yt-built-in-charts.png)](https://www.youtube.com/watch?v=x0eZoQDnAPA) +[![Understanding TelemetryDeck's built-in charts](/assets/yt-built-in-charts.png)](https://www.youtube.com/watch?v=x0eZoQDnAPA) ### Versatile and adjustable @@ -53,12 +53,12 @@ The title is displayed on top of any insight card. Example: "Daily Active Users" **Compact and wide mode** You can choose between "Compact" and "Wide". If "Wide" is active, the insight will be displayed in a larger frame. This allows you to see your most important insight with extra detail. -![buttons for the compact mode](/docs/images/compact_wide_mode.PNG) +![buttons for the compact mode](/assets/compact_wide_mode.PNG) **Display Mode** Here, you can choose the most suitable chart type for your insight. Learn more about the chart types in the section [Chart Types](#chart-types). -![icons for the display mode](/docs/images/display_mode.PNG) +![icons for the display mode](/assets/display_mode.PNG) **Counting** If "Count Signals" is checked, an insight will count all signals by their type. If "Count Users" is checked, each user will only get counted once. Only the newest is selected if the set of signals counted by the insight contains multiple signals from the same user. This is great for counting users, for example, "How many users are using feature x?" @@ -106,7 +106,7 @@ There is no going back from here. If you later decide that there is no custom qu **Check out our [YouTube Channel](https://www.youtube.com/watch?v=PYWO3017_Ho) on how to create custom charts!** -[![Creating Custom Charts in TelemetryDeck – Conversion Tracking Tutorial](/docs/images/yt-custom-charts.png)](https://www.youtube.com/watch?v=PYWO3017_Ho) +[![Creating Custom Charts in TelemetryDeck – Conversion Tracking Tutorial](/assets/yt-custom-charts.png)](https://www.youtube.com/watch?v=PYWO3017_Ho) ## Chart Types @@ -119,7 +119,7 @@ The TelemetryDeck dashboard will show time zones of your users in **their browse --- -![An example of a table chart](/docs/images/table-chart.PNG) +![An example of a table chart](/assets/table-chart.PNG) **Table** @@ -129,7 +129,7 @@ If only one line of data is returned, that line will be displayed in large type. --- -![An example of a bar chart](/docs/images/bar-chart.PNG) +![An example of a bar chart](/assets/bar-chart.PNG) **Bar Chart** @@ -137,7 +137,7 @@ It displays the query result as a vertical bar chart, one bar per line of data. --- -![An example of a line chart](/docs/images/line-chart.PNG) +![An example of a line chart](/assets/line-chart.PNG) **Line Chart** @@ -145,7 +145,7 @@ This chart shows the query result as a line chart, one bar per line of data. Thi --- -![An example of a donut chart](/docs/images/donut-chart.PNG) +![An example of a donut chart](/assets/donut-chart.PNG) **Donut Chart** @@ -153,7 +153,7 @@ A pie/donut type chart that will sort the rows of data by number and visually sh --- -![An example of a funnel chart](/docs/images/funnels_example.png) +![An example of a funnel chart](/assets/funnels_example.png) **Funnel Chart** diff --git a/docs/articles/login-options.md b/docs/articles/login-options.md index 0ab72c4..746a8b6 100644 --- a/docs/articles/login-options.md +++ b/docs/articles/login-options.md @@ -18,7 +18,7 @@ To login into the TelemetryDeck dashboard you can use any of these options: * Okta (for our enterprise customers, on request) -![Login mask](/docs/images/login-mask.png) +![Login mask](/assets/login-mask.png) ## Frequently Asked Questions **Does TemeletryDeck support 2FA?** diff --git a/docs/articles/notebooks.md b/docs/articles/notebooks.md index f193311..043d6b8 100644 --- a/docs/articles/notebooks.md +++ b/docs/articles/notebooks.md @@ -16,7 +16,7 @@ searchEngineDescription: Learn how to effectively use TelemetryDeck Notebooks to Notebooks are your analytics lab notebook – a place where you can document your findings, share insights with your team, and maintain context throughout your investigations. Let's explore how to use them effectively. {% noteinfo "See Notebooks in action" %} -[![Notebooks Feature Demo - Learn how to combine live charts with markdown text for better analytics insights](/docs/images/notebooks-video-thumbnail.png)](https://www.youtube.com/watch?v=WAa2BRIaVGE) +[![Notebooks Feature Demo - Learn how to combine live charts with markdown text for better analytics insights](/assets/notebooks-video-thumbnail.png)](https://www.youtube.com/watch?v=WAa2BRIaVGE) *Watch the [Notebooks Walkthrough](https://www.youtube.com/watch?v=WAa2BRIaVGE) on our YouTube channel* {% endnoteinfo %} @@ -32,7 +32,7 @@ Notebooks are your analytics lab notebook – a place where you can document you Starting with a clear question or hypothesis helps focus your analysis and makes it easier to structure your notebook. It also makes it easier to share your findings with others later. {% endnoteinfo %} -![A screenshot of the Notebooks overview tab, showing a list of notebooks](/docs/images/Notebooks-Overview.png) +![A screenshot of the Notebooks overview tab, showing a list of notebooks](/assets/Notebooks-Overview.png) ### Adding charts to your notebook @@ -62,7 +62,7 @@ You can customize how your data is displayed using the `displayMode` value in yo - `raw` - Ideal for detailed breakdowns - `funnelChart` - Essential for conversion analysis -![A screenshot showing the markdown editor with TQL code, and the live chart preview in a notebook](/docs/images/Notebooks-TQL-Live-Preview.png) +![A screenshot showing the markdown editor with TQL code, and the live chart preview in a notebook](/assets/Notebooks-TQL-Live-Preview.png) ### Using Markdown effectively diff --git a/docs/articles/preset-purchases.md b/docs/articles/preset-purchases.md index fbeb9cd..9f2256f 100644 --- a/docs/articles/preset-purchases.md +++ b/docs/articles/preset-purchases.md @@ -109,4 +109,4 @@ You can answer all subsequent questions with "No" because we neither link collec When all is configured your "Purchases" entry in your App Privacy page should end up looking like this: -![Purchases entry with only 'Used for Analytics' in the box](/docs/images/purchases-privacy-box.png) +![Purchases entry with only 'Used for Analytics' in the box](/assets/purchases-privacy-box.png) diff --git a/docs/articles/set-up-filters-insights.md b/docs/articles/set-up-filters-insights.md index b48ad8b..c467918 100644 --- a/docs/articles/set-up-filters-insights.md +++ b/docs/articles/set-up-filters-insights.md @@ -16,7 +16,7 @@ With filters, you can create custom views of your insights and organize the info Navigate to the [dashboard](https://dashboard.telemetrydeck.com), and select your app. You can find the filter editor in your insight group, while editing an insight. Either create a new insight or edit an existing one to see the editor just below the chart itself. -![Screenshot of the filter location in the dashboard](/docs/images/filter_location.png) +![Screenshot of the filter location in the dashboard](/assets/filter_location.png) In your insight group, you can add filters to any insight you want. With advanced filter options available, you can add multiple filters to obtain the desired query results. Nesting filters and combining them with boolean operations can help you refine your query and achieve even more accurate insights. @@ -28,7 +28,7 @@ You can use analytics filters to refine your data and obtain precise insights. W - The **Selector** filter lets you compare any _payload key_ (also known as a dimension) to a value. For instance, if you send a `shouldUseHealthKit` parameter as part of your payload in signals, you can filter for `shouldUseHealthKit = “true”`. - Additionally, you can use **Regular Expressions** (RegEx) to filter data based on _complex patterns or strings of text_. Sometimes it is not possible to match a value exactly, and in such cases, regular expressions can be incredibly useful to obtain more accurate and specific insights into user behavior. A good example for using RegEx is filtering by device type, locale, or anything else you would like a more precise view on! -![Screenshot of how adding a condition changes the output. Adding that only iOS versions should be shown, and then wrapping the condition inside a NOT so that everything but iOS versions is shown.](/docs/images/filter_example.png) +![Screenshot of how adding a condition changes the output. Adding that only iOS versions should be shown, and then wrapping the condition inside a NOT so that everything but iOS versions is shown.](/assets/filter_example.png) ## Query construction diff --git a/docs/articles/test-mode.md b/docs/articles/test-mode.md index 528b77b..7c67e95 100644 --- a/docs/articles/test-mode.md +++ b/docs/articles/test-mode.md @@ -28,7 +28,7 @@ Each sent signal has a `isTestMode` parameter, which can either be `true` or `fa Navigate to your TelemetryDeck [dashboard](https://dashboard.telemetrydeck.com/), where you will find the Test Mode toggle on the top left side, just above the sidebar. You can toggle it to show your signals either in `isTestMode == true` or `isTestMode == false`. While toggled to `true`, you will see a banner at the top displaying **Test Data** to remind you that you are currently in Test Mode, and all signals get sent in said mode. All charts will display test data only while in Test Mode. -![Screenshot of the dashboard showing the Test Mode toggle in the upper left corner.](/docs/images/test_mode.png) +![Screenshot of the dashboard showing the Test Mode toggle in the upper left corner.](/assets/test_mode.png) ## Sending Signals in Test Mode diff --git a/docs/articles/update-package.md b/docs/articles/update-package.md index b0ceba9..4a6d60e 100644 --- a/docs/articles/update-package.md +++ b/docs/articles/update-package.md @@ -25,7 +25,7 @@ With that in mind, here's how to manually update a Swift package using Xcode: ## How to update a Swift Package in Xcode -![Screenshot of the left sidebar in Xcode 14, with the TelemetryDeck Swift Package highlighted](/docs/images/update_package.png) +![Screenshot of the left sidebar in Xcode 14, with the TelemetryDeck Swift Package highlighted](/assets/update_package.png) Updating packages in Xcode can be done using one of two ways. If you want to just update a single package individually – which we recommend by the way – do this: @@ -45,7 +45,7 @@ By default, Xcode and Swift Package Manager will not update the Major version of If you want to update to the next major version of a package, you can do this by manually changing the version requirement in the Xcode project. To do that, navigate to the project, select "Package Dependencies" and manually update the minimum version requirement to the next major version. -![Screenshot of the left sidebar in Xcode 14, with the TelemetryDeck Swift Package highlighted](/docs/images/update_next_major_version.png) +![Screenshot of the left sidebar in Xcode 14, with the TelemetryDeck Swift Package highlighted](/assets/update_next_major_version.png) For example, if you have a package that is currently at version `1.2.3`, and you want to update to version `2.0.0`, you can change the version requirement in the Xcode project to `2.0.0`. This will tell the Swift Package Manager to download the newest version of the package that is greater than `2.0.0`, but less than `3.0.0`. When a `2.0.1` comes out, you can update to that version by following the steps above. diff --git a/docs/basics/acquisition.md b/docs/basics/acquisition.md index 2e61e7e..454707b 100644 --- a/docs/basics/acquisition.md +++ b/docs/basics/acquisition.md @@ -30,7 +30,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - Are there patterns in my acquisition rate? - How effective are my marketing campaigns? -![Daily, Weekly, and Monthly New Users](/docs/images/acquisition-new-users.png) +![Daily, Weekly, and Monthly New Users](/assets/acquisition-new-users.png) **How to interpret the charts:** - **Consistent upward trend**: Your app is steadily gaining users – investigate what's working and double down @@ -47,7 +47,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - Is my app primarily attracting new users or maintaining existing ones? - How does acquisition compare to retention? -![Active vs New Users Ratio](/docs/images/acquisition-active-vs-new.png) +![Active vs New Users Ratio](/assets/acquisition-active-vs-new.png) **How to interpret the chart:** - **High ratio (>0.5)**: Many new users compared to your active base - suggesting strong acquisition but possibly weak retention @@ -63,7 +63,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - Which days of the week see the most new installations? - Do weekdays significantly outperform weekends for user acquisition? -![Acquisition Time Patterns](/docs/images/acquisition-by-time.png) +![Acquisition Time Patterns](/assets/acquisition-by-time.png) **How to interpret the charts:** - **Hour of day peaks**: Show prime discovery times (user-local) @@ -79,7 +79,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - Have specific events or campaigns created temporary spikes? - How can I differentiate between one-off events and genuine trends? -![Recent Acquisition Time Patterns](/docs/images/acquisition-by-time-last-weeks.png) +![Recent Acquisition Time Patterns](/assets/acquisition-by-time-last-weeks.png) **How to interpret the charts:** - **Four-week hourly view**: Identifies specific days with unusual activity @@ -95,7 +95,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - Should I prioritize specific hardware or screen sizes? - When can I safely drop support for older devices? -![New Users by Device Type](/docs/images/acquisition-device-distribution.png) +![New Users by Device Type](/assets/acquisition-device-distribution.png) **How to interpret the chart:** - **Top device models**: Prioritize testing and optimization for these devices @@ -111,7 +111,7 @@ New users are detected by the `TelemetryDeck.Acquisition.newInstallDetected` sig - What languages do my users speak? - Which markets should I prioritize for expansion? -![New Users by Country and Language](/docs/images/acquisition-geographic-distribution.png) +![New Users by Country and Language](/assets/acquisition-geographic-distribution.png) **How to interpret the charts:** - **Dominant regions**: Countries with the largest segments indicate your strongest markets diff --git a/docs/basics/activation.md b/docs/basics/activation.md index 4642b96..d17191e 100644 --- a/docs/basics/activation.md +++ b/docs/basics/activation.md @@ -31,7 +31,7 @@ Activated users are those with at least 5 minutes accumulated total usage time w - How effective is my onboarding experience? - What are the immediate activation patterns in the last 24 hours? -![Hourly, Daily, Weekly, and Monthly Activated Users](/docs/images/activation-activated-users.png) +![Hourly, Daily, Weekly, and Monthly Activated Users](/assets/activation-activated-users.png) **How to interpret the charts:** - **Hourly (last 24h)**: Shows recent activation activity and immediate trends – useful for spotting issues or validating recent changes @@ -49,7 +49,7 @@ Activated users are those with at least 5 minutes accumulated total usage time w - Do weekdays or weekends provide better conditions for user activation? - Are recent activation patterns consistent with long-term trends? -![Activation Time Patterns](/docs/images/activation-by-time.png) +![Activation Time Patterns](/assets/activation-by-time.png) **How to interpret the charts:** - **Hour of day patterns**: Show when users have time to properly explore your app (user-local timezone) @@ -66,7 +66,7 @@ Activated users are those with at least 5 minutes accumulated total usage time w - Should I optimize my Onboarding for specific hardware or screen sizes? - Are there platform-specific activation barriers? -![Activated Users by Device Type](/docs/images/activation-device-distribution.png) +![Activated Users by Device Type](/assets/activation-device-distribution.png) **How to interpret the chart:** - **Top-performing devices**: These provide the best activation experience – understand why @@ -83,7 +83,7 @@ Several other models follow at about 5.71% each. This fairly even split suggests - What languages correlate with higher activation rates? - Which markets provide the strongest foundation for growth? -![Activated Users by Country and Language](/docs/images/activation-geographic-distribution.png) +![Activated Users by Country and Language](/assets/activation-geographic-distribution.png) **How to interpret the charts:** - **Strong activation regions**: Countries where users consistently experience your app's value diff --git a/docs/basics/index.md b/docs/basics/index.md index eb880ad..1791928 100644 --- a/docs/basics/index.md +++ b/docs/basics/index.md @@ -16,7 +16,7 @@ order: 10 After integrating the TelemetryDeck SDK into your app, your data will start appearing in the TelemetryDeck dashboard. The main navigation tabs help you find different types of insights: -![TelemetryDeck Main Navigation](/docs/images/dashboard-main-nav.png) +![TelemetryDeck Main Navigation](/assets/dashboard-main-nav.png) Let's explore what you'll find in each section: @@ -24,7 +24,7 @@ Let's explore what you'll find in each section: The Overview tab provides a high-level summary of your app's performance, showing key metrics like daily active users, total users, user retention, app version distribution, and system version distribution: -![Overview Dashboard](/docs/images/dashboard-overview.png) +![Overview Dashboard](/assets/dashboard-overview.png) This view gives you immediate answers to: "How is my app performing right now?" and "Are my users upgrading to new versions?" @@ -32,7 +32,7 @@ This view gives you immediate answers to: "How is my app performing right now?" The Customers section organizes your user data according to the journey users take with your app, from discovery to monetization: -![Acquisition Dashboard](/docs/images/dashboard-acquisition.png) +![Acquisition Dashboard](/assets/dashboard-acquisition.png) Each tab focuses on a different stage of the user journey: @@ -110,7 +110,7 @@ TelemetryDeck also provides built-in presets for tracking purchases and revenue The Metrics section provides essential technical insights about your app across different devices and platforms: -![Metrics Dashboard - Devices](/docs/images/metrics-devices.png) +![Metrics Dashboard - Devices](/assets/metrics-devices.png) **Key metrics include:** @@ -139,7 +139,7 @@ The Metrics section provides essential technical insights about your app across The Explore section gives you direct access to your raw event data through multiple views: -![Explore Dashboard - Signal Types](/docs/images/dashboard-signal-types.png) +![Explore Dashboard - Signal Types](/assets/dashboard-signal-types.png) Here you can examine all signal types your app has sent, explore parameters attached to your events, view recent events chronologically, and use the query playground to experiment with data analysis. This section is particularly useful for debugging, understanding user behavior sequences, and diving deeper into specific user actions. @@ -162,7 +162,7 @@ Here you can examine all signal types your app has sent, explore parameters atta The Dashboards section allows you to create and organize custom visualizations: -![Custom Dashboards](/docs/images/dashboard-custom.png) +![Custom Dashboards](/assets/dashboard-custom.png) You can build insights using either the visual editor (for simple queries) or TelemetryDeck Query Language (TQL) for more complex analysis. Common dashboard setups include: @@ -223,7 +223,7 @@ Each insight can be organized into groups (displayed in the sidebar) to keep rel The Notebooks tab lets you combine live charts and markdown text in a single place, so you can document your questions, structure your investigations, and share insights with your team. Perfect for keeping context during long-running analyses, preparing presentations, or leaving notes for your future self. For example, here's an excerpt of a notebook: -![Sample Notebook investigating Onboarding issues with Explanations](/docs/images/dashboard-notebooks.png) +![Sample Notebook investigating Onboarding issues with Explanations](/assets/dashboard-notebooks.png) You can use Notebooks to: - Document your hypotheses and findings alongside live data @@ -253,7 +253,7 @@ See Notebooks in action in our [feature demo video](https://www.youtube.com/watc At the top of most dashboard pages, you'll find time filters that let you focus on specific periods: -![Filtering Data](/docs/images/dashboard-filtering-data.png) +![Filtering Data](/assets/dashboard-filtering-data.png) - **Last 30 Days** (default) – Shows data from the past month - **Test Mode** – Toggle to see data from development builds (when your app is run directly from your IDE) @@ -277,7 +277,7 @@ At the top of most dashboard pages, you'll find time filters that let you focus Many visualizations have interactive elements: -![Exploring Details](/docs/images/dashboard-exploring-details.png) +![Exploring Details](/assets/dashboard-exploring-details.png) - Hover over chart elements to see detailed values and source info - Use the "…" menu in the top right of cards for additional options diff --git a/docs/basics/metrics.md b/docs/basics/metrics.md index f5c438a..04e3d00 100644 --- a/docs/basics/metrics.md +++ b/docs/basics/metrics.md @@ -26,7 +26,7 @@ With TelemetryDeck's Metrics dashboard, these insights are automatically collect - Which platforms should I prioritize for development and testing? - When can I safely drop support for older devices? -![Device and Platform Distribution](/docs/images/metrics-devices.png) +![Device and Platform Distribution](/assets/metrics-devices.png) **How to interpret the charts:** - **Models**: Shows specific device models being used, helping prioritize testing @@ -44,7 +44,7 @@ With TelemetryDeck's Metrics dashboard, these insights are automatically collect - Are there patterns in build adoption? - Which SDK versions are in use across your user base? -![Version Analytics](/docs/images/metrics-versions.png) +![Version Analytics](/assets/metrics-versions.png) **How to interpret the charts:** - **App Versions**: Tracks adoption of your app releases over time @@ -61,7 +61,7 @@ With TelemetryDeck's Metrics dashboard, these insights are automatically collect - Which errors should I prioritize fixing? - Are errors occurring on specific devices or platforms? -![Error Monitoring](/docs/images/metrics-errors.png) +![Error Monitoring](/assets/metrics-errors.png) **How to interpret the charts:** - **Most Frequent Errors**: Ranks issues by occurrence count and percentage @@ -98,7 +98,7 @@ TelemetryDeck offers built-in presets for error tracking that require some confi - Should I invest in additional localizations? - Are users using my app in the same language as their device? -![Localization Insights](/docs/images/metrics-localization.png) +![Localization Insights](/assets/metrics-localization.png) **How to interpret the charts:** - **Preferred Language**: Shows user device language settings @@ -116,7 +116,7 @@ TelemetryDeck offers built-in presets for error tracking that require some confi - Does my app need better accessibility support? - How would design changes impact users with accessibility needs? -![Accessibility Usage](/docs/images/metrics-accessibility.png) +![Accessibility Usage](/assets/metrics-accessibility.png) **How to interpret the charts:** - **Preferred Content Size**: Shows text size preferences (88.43% use default size "L") diff --git a/docs/basics/pirate-metrics.md b/docs/basics/pirate-metrics.md index 01b179b..6436068 100644 --- a/docs/basics/pirate-metrics.md +++ b/docs/basics/pirate-metrics.md @@ -18,7 +18,7 @@ Pirate Metrics, also known as the AARRR framework, was created by venture capita This framework breaks down the complete user journey into five key stages that form a natural progression: -![AARRR Framework Chart](/docs/images/aarrr-framework.png) +![AARRR Framework Chart](/assets/aarrr-framework.png) 1. **Acquisition**: How users discover and install your app 2. **Activation**: How users experience your app for the first time diff --git a/docs/basics/retention.md b/docs/basics/retention.md index 7e83c31..6abd1d7 100644 --- a/docs/basics/retention.md +++ b/docs/basics/retention.md @@ -31,7 +31,7 @@ Returning users are those who have used your app for more than 5 sessions. - What are the immediate retention patterns in the last 24 hours? - How do retention trends compare across different time periods? -![Hourly, Daily, Weekly, and Monthly Returning Users](/docs/images/retention-returning-users.png) +![Hourly, Daily, Weekly, and Monthly Returning Users](/assets/retention-returning-users.png) **How to interpret the charts:** - **Hourly (last 24h)**: Shows recent returning user activity – useful for immediate validation of retention initiatives @@ -49,7 +49,7 @@ Returning users are those who have used your app for more than 5 sessions. - Do returning users prefer weekdays or weekends for app usage? - Are retention patterns changing over recent weeks? -![Retention Time Patterns](/docs/images/retention-by-time.png) +![Retention Time Patterns](/assets/retention-by-time.png) **How to interpret the charts:** - **Hour of day patterns**: Show when returning users are most active (user-local timezone) @@ -66,7 +66,7 @@ Returning users are those who have used your app for more than 5 sessions. - Are there platform-specific retention challenges? - Should I prioritize retention improvements for specific hardware? -![Returning Users by Device Type](/docs/images/retention-device-distribution.png) +![Returning Users by Device Type](/assets/retention-device-distribution.png) **How to interpret the charts:** - **Top-retention devices**: These provide the best long-term user experience @@ -82,7 +82,7 @@ Returning users are those who have used your app for more than 5 sessions. - What languages correlate with higher long-term engagement? - Are there cultural or regional patterns in user retention? -![Returning Users by Country and Language](/docs/images/retention-geographic-distribution.png) +![Returning Users by Country and Language](/assets/retention-geographic-distribution.png) **How to interpret the charts:** - **High-retention regions**: Countries where users consistently return to your app diff --git a/docs/guides/objective-c-setup.md b/docs/guides/objective-c-setup.md index 3693579..5258698 100644 --- a/docs/guides/objective-c-setup.md +++ b/docs/guides/objective-c-setup.md @@ -27,7 +27,7 @@ The TelemetryDeck Swift package uses Swift Package Manager. 1. Set the Dependency Rule to Up to Next Major Version. 1. Click Add Package. -![A screenshot of Xcode adding the TelemetryDeck Package](/docs/images/xcode-swift-package.png) +![A screenshot of Xcode adding the TelemetryDeck Package](/assets/xcode-swift-package.png) This will include the TelemetryDeck Swift Client into your app by downloading the source code. Feel free to browse the client's source code. It's tiny and you'll see for yourself how TelemetryDeck is hashing user identifiers before they ever reach the server. Privacy, yay! diff --git a/docs/guides/privacy-faq.md b/docs/guides/privacy-faq.md index 975e825..347089b 100644 --- a/docs/guides/privacy-faq.md +++ b/docs/guides/privacy-faq.md @@ -7,7 +7,7 @@ featured: true description: Frequently asked questions about TelemetryDeck's Privacy Policy lead: This FAQ answers the most common questions about our Privacy Policy and how to answer your users' questions. order: 10000 -headerImage: /docs/images/faq.jpg +headerImage: /assets/faq.jpg --- The TelemetryDeck SDK was developed from the ground up with privacy in mind. An important principle of TelemetryDeck is to store only the minimum amount of data necessary to identify specific user behaviors. diff --git a/docs/guides/react-setup.md b/docs/guides/react-setup.md index 5ff1982..c7d851e 100644 --- a/docs/guides/react-setup.md +++ b/docs/guides/react-setup.md @@ -161,7 +161,7 @@ Now that you've integrated TelemetryDeck, learn how to use the analytics platfor ## Sponsors -[](https://typedigital.de) +[](https://typedigital.de) The development of the TelemetryDeck React SDK was graciously provided by our friends at Augsburg-based web development agency [typedigital](https://typedigital.de). Thanks a lot, and check them out for your web application development needs. 🧡 diff --git a/docs/guides/swift-setup.md b/docs/guides/swift-setup.md index 9af5d71..5e96b6d 100644 --- a/docs/guides/swift-setup.md +++ b/docs/guides/swift-setup.md @@ -28,19 +28,19 @@ The TelemetryDeck Swift package uses Swift Package Manager. 1. Set the Dependency Rule to Up to Next Major Version 1. Press Add Package to continue -![A screenshot of Xcode adding the TelemetryDeck Package](/docs/images/xcode-swift-package1.png) +![A screenshot of Xcode adding the TelemetryDeck Package](/assets/xcode-swift-package1.png) This will include the TelemetryDeck Swift SDK into your app by downloading the source code. Feel free to browse the client's source code, it's tiny and you'll see for yourself how TelemetryDeck is hashing user identifiers before they ever reach the server. Privacy, yay! **Watch our [Quick Start video](https://www.youtube.com/watch?v=FA71bSnK_B8) to setup TelemetryDeck in 4 Minutes!** -[![TelemetryDeck Setup in 4 Minutes – Swift SDK Integration](/docs/images/yt-4-minute-setup.png)](https://www.youtube.com/watch?v=FA71bSnK_B8) +[![TelemetryDeck Setup in 4 Minutes – Swift SDK Integration](/assets/yt-4-minute-setup.png)](https://www.youtube.com/watch?v=FA71bSnK_B8) ## Including the package in your target Xcode will ask you to link the package with your target in the next screen, titled Choose Package Products for SwiftSDK. Set the Add to target column to your app target for TelemetryDeck (not "TelemetryClient", which is deprecated) and click Add Package to complete the integration. -![A screenshot of Xcode setting the target for the TelemetryDeck library](/docs/images/xcode-swift-package2.png) +![A screenshot of Xcode setting the target for the TelemetryDeck library](/assets/xcode-swift-package2.png) {% noteinfo "Link Library with more than one Target" %} diff --git a/docs/guides/vue-setup.md b/docs/guides/vue-setup.md index 6d58dc8..dba9fd7 100644 --- a/docs/guides/vue-setup.md +++ b/docs/guides/vue-setup.md @@ -153,7 +153,7 @@ Now that you've integrated TelemetryDeck, learn how to use the analytics platfor ## Sponsors -[](https://www.peerigon.com) +[](https://www.peerigon.com) The development of the TelemetryDeck Vue SDK was graciously provided by our friends at Augsburg-based bespoke software development company [Peerigon](https://www.peerigon.com). Thanks a lot, and check them out for your application development needs. 🧡 diff --git a/docs/integrations/revenuecat.md b/docs/integrations/revenuecat.md index 8a4e73b..022d834 100644 --- a/docs/integrations/revenuecat.md +++ b/docs/integrations/revenuecat.md @@ -93,7 +93,7 @@ Now we need to tell RevenueCat to send copies of all events over to TelemetryDec - Select **TelemetryDeck** - Click **Add Integration** to confirm -![A screenshot of RevenueCat's TelemetryDeck Integration](/docs/images/rc-td-1.png) -![A screenshot of RevenueCat's TelemetryDeck Integration](/docs/images/rc-td-2.png) +![A screenshot of RevenueCat's TelemetryDeck Integration](/assets/rc-td-1.png) +![A screenshot of RevenueCat's TelemetryDeck Integration](/assets/rc-td-2.png) And you're done. RevenueCat events should now arrive and be mixed in with your TelemetryDeck signals 🥳 From 4af87ee328384f22889e3b3d1d86b5950a5ff04c Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Sun, 10 May 2026 12:37:37 +0200 Subject: [PATCH 14/16] feat: try grouping sections --- zensical.toml | 130 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 9 deletions(-) diff --git a/zensical.toml b/zensical.toml index 22920b5..cd9a34a 100644 --- a/zensical.toml +++ b/zensical.toml @@ -41,16 +41,128 @@ Copyright © 2026 TelemetryDeck GmbH """ # Zensical supports both implicit navigation and explicitly defined navigation. -# If you decide not to define a navigation here then Zensical will simply -# derive the navigation structure from the directory structure of your -# "docs_dir". The definition below demonstrates how a navigation structure -# can be defined using TOML syntax. +# Pages are grouped thematically below; group titles map to TOML inline tables, +# and a value can be either a path (single page) or an array (collapsible group). # # Read more: https://zensical.org/docs/setup/navigation/ -# nav = [ -# { "Get started" = "index.md" }, -# { "Markdown in 5min" = "markdown.md" }, -# ] +nav = [ + { "Welcome" = "index.md" }, + { "SDK Setup" = [ + { "Swift" = "guides/swift-setup.md" }, + { "Android (Kotlin)" = "guides/android-setup.md" }, + { "Flutter" = "guides/flutter-setup.md" }, + { "JavaScript" = "guides/javascript-setup.md" }, + { "React" = "guides/react-setup.md" }, + { "Vue" = "guides/vue-setup.md" }, + { "Web" = "guides/web-setup.md" }, + { "Objective-C" = "guides/objective-c-setup.md" }, + { "Privacy FAQ" = "guides/privacy-faq.md" }, + ] }, + { "Analytics Basics" = [ + { "Overview" = "basics/index.md" }, + { "Acquisition" = "basics/acquisition.md" }, + { "Activation" = "basics/activation.md" }, + { "Retention" = "basics/retention.md" }, + { "Metrics" = "basics/metrics.md" }, + { "Pirate Metrics (AARRR)" = "basics/pirate-metrics.md" }, + { "Built-in Presets" = [ + { "Purchases" = "articles/preset-purchases.md" }, + { "Errors" = "articles/preset-errors.md" }, + { "Navigation signals" = "articles/navigation-signals.md" }, + { "Duration signals" = "articles/duration-signals.md" }, + ] }, + ] }, + { "Insights & Dashboards" = [ + { "Insights overview" = "articles/insights.md" }, + { "Daily active users" = "articles/insights-about-daily-users.md" }, + { "Monthly active users" = "articles/insights-about-monthly-users.md" }, + { "Referrers" = "articles/insights-about-referrers.md" }, + { "System version distribution" = "articles/insights-about-system-version.md" }, + { "Latest version adoption" = "articles/check-if-users-upgrade-to-latest-app-version.md" }, + { "Funnel insights" = "articles/how-to-funnel-insights.md" }, + { "Filter setup" = "articles/set-up-filters-insights.md" }, + { "Custom dashboards" = "articles/create-custom-dashboards.md" }, + { "Notebooks" = "articles/notebooks.md" }, + ] }, + { "Signal Concepts" = [ + { "Namespaces" = "articles/namespaces.md" }, + { "Signal type naming" = "articles/signal-type-naming.md" }, + { "Test mode" = "articles/test-mode.md" }, + { "Updating the SDK" = "articles/update-package.md" }, + { "The Grand Rename" = "articles/grand-rename.md" }, + { "TelemetryClient (legacy)" = "articles/telemetry-client.md" }, + ] }, + { "Privacy & Compliance" = [ + { "How anonymization works" = "articles/anonymization-how-it-works.md" }, + { "Apple App Privacy" = "articles/apple-app-privacy.md" }, + { "App Privacy Report" = "articles/app-privacy-report.md" }, + { "App Tracking Transparency" = "articles/app-tracking-transparency.md" }, + { "Hosting solutions" = "articles/hosting-solutions.md" }, + ] }, + { "Account & Organization" = [ + { "Make an account" = "articles/making-account.md" }, + { "Invite members" = "articles/invite-members-to-organization.md" }, + { "Login options" = "articles/login-options.md" }, + ] }, + { "Recipes" = [ + { "Averaging numbers" = "recipes/averagaging-numbers.md" }, + { "Listing events" = "recipes/listing-events.md" }, + { "Views per purchase" = "recipes/views-per-purchase-example.md" }, + ] }, + { "Integrations" = [ + { "RevenueCat" = "integrations/revenuecat.md" }, + { "Superwall" = "integrations/superwall.md" }, + { "Google Tag Manager" = "integrations/web-setup-google-tag-manager.md" }, + ] }, + { "Server-side Ingest" = [ + { "Default parameters" = "ingest/default-parameters.md" }, + { "Ingest API v2" = "ingest/v2.md" }, + { "Ingest API v1 (legacy)" = "ingest/v1.md" }, + ] }, + { "Reference" = [ + { "Signals API" = [ + { "Signals reference" = "api/signals-reference.md" }, + { "Insights reference" = "api/insights-reference.md" }, + { "Run a query" = "api/api-run-query.md" }, + { "Query from insight" = "api/api-query-from-insight.md" }, + { "API tokens" = "api/api-token.md" }, + ] }, + { "TQL" = [ + { "TQL guidelines" = "tql/firstGuideline.md" }, + { "TQL query reference" = "tql/query.md" }, + { "Query context" = "tql/queryContext.md" }, + { "Query type" = "tql/queryType.md" }, + { "Query types" = [ + { "Timeseries" = "tql/timeseries.md" }, + { "Top N" = "tql/topN.md" }, + { "Group By" = "tql/groupBy.md" }, + { "Scan" = "tql/scan.md" }, + { "Funnel" = "tql/funnel.md" }, + { "Retention" = "tql/retention.md" }, + { "Experiment" = "tql/experiment.md" }, + { "Funnels (deprecated)" = "tql/funnels.md" }, + ] }, + { "Filters & time" = [ + { "Filters" = "tql/filters.md" }, + { "Base filters" = "tql/baseFilters.md" }, + { "Time intervals" = "tql/time-intervals.md" }, + ] }, + { "Aggregators" = [ + { "Aggregators" = "tql/aggregators.md" }, + { "Post-aggregators" = "tql/post-aggregators.md" }, + ] }, + { "Specs & helpers" = [ + { "Dimension spec" = "tql/dimensionSpec.md" }, + { "Extraction function" = "tql/extractionFunction.md" }, + { "Granularity" = "tql/granularity.md" }, + { "TopN metric spec" = "tql/topNMetricSpec.md" }, + { "Value formatter" = "tql/valueFormatter.md" }, + { "Descending" = "tql/descending.md" }, + ] }, + ] }, + ] }, + { "Glossary" = "glossary.md" }, +] # With the "extra_css" option you can add your own CSS styling to customize # your Zensical project according to your needs. You can add any number of @@ -213,7 +325,7 @@ features = [ # When sections are enabled, top-level sections are rendered as groups in # the sidebar for viewports above 1220px, but remain as-is on mobile. # https://zensical.org/docs/setup/navigation/#navigation-sections - "navigation.sections", + #"navigation.sections", # When tabs are enabled, top-level sections are rendered in a menu layer # below the header for viewports above 1220px, but remain as-is on mobile. From 5e8671b123ef0404e55185b2ed561d8dd1f8126e Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Sun, 10 May 2026 12:46:18 +0200 Subject: [PATCH 15/16] feat: try serving markdown --- ...-static-web-apps-purple-bush-012fe8010.yml | 2 + .github/workflows/docs.yml | 2 + README.md | 10 + scripts/build_llms.py | 255 ++++++++++++++++++ staticwebapp.config.json | 6 + 5 files changed, 275 insertions(+) create mode 100644 scripts/build_llms.py create mode 100644 staticwebapp.config.json diff --git a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml index 593baec..1518d01 100644 --- a/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml +++ b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml @@ -29,6 +29,8 @@ jobs: python-version: 3.x - run: pip install zensical - run: zensical build --clean + - name: Generate llms.txt and ship raw markdown + run: python3 scripts/build_llms.py - name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 47ee722..70ed450 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,6 +22,8 @@ jobs: python-version: 3.x - run: pip install zensical - run: zensical build --clean + - name: Generate llms.txt and ship raw markdown + run: python3 scripts/build_llms.py - uses: actions/upload-pages-artifact@v5 with: path: site diff --git a/README.md b/README.md index 4ca93c3..6de2adb 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,13 @@ Check out `articles/documentation.md` for an overview of how documentation pages We use Python `pip`. You can follow the setups at https://zensical.org/docs/get-started/#installation for the initial setup. Note: Make sure the Python version is recent e.g. 3.14 or later (`python3 --version`). + +## LLM and agent friendly output + +Every page is also published as raw Markdown alongside the rendered HTML. After `zensical build`, run `python3 scripts/build_llms.py` to: + +- copy each source `.md` from `docs/` into the corresponding location in `site/` (so `https://docs.telemetrydeck.com/articles/insights.md` returns the source for `/articles/insights/`), +- write `site/llms.txt` following the [llms.txt](https://llmstxt.org) convention, +- write `site/llms-full.txt` containing every page concatenated for one-shot ingestion. + +Both deploy workflows run this step automatically after the Zensical build. diff --git a/scripts/build_llms.py b/scripts/build_llms.py new file mode 100644 index 0000000..97a4da0 --- /dev/null +++ b/scripts/build_llms.py @@ -0,0 +1,255 @@ +"""Generate llms.txt, llms-full.txt, and ship raw .md sources alongside the +rendered HTML so coding agents and LLMs can consume the documentation. + +Run after `zensical build`. Reads: + - zensical.toml (for site metadata and navigation) + - docs/ (source markdown) + +Writes into the existing site/ directory: + - site/.md for every source page (preserves directory layout) + - site/llms.txt structured index per https://llmstxt.org + - site/llms-full.txt all page content concatenated for one-shot consumption +""" + +from __future__ import annotations + +import re +import shutil +import sys +import tomllib +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + +ROOT = Path(__file__).resolve().parent.parent +CONFIG_PATH = ROOT / "zensical.toml" +DOCS_DIR = ROOT / "docs" +SITE_DIR = ROOT / "site" +STATIC_WEB_APP_CONFIG = ROOT / "staticwebapp.config.json" +DEFAULT_SITE_URL = "https://docs.telemetrydeck.com" + +FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL) + + +@dataclass +class Page: + rel_path: str + title: str + description: str + + +def load_config() -> dict: + with CONFIG_PATH.open("rb") as f: + return tomllib.load(f) + + +def parse_frontmatter(text: str) -> tuple[dict, str]: + """Extract YAML-ish frontmatter without pulling in PyYAML. + + Only top-level scalar key/value pairs are read — that is all we need + (title, description, lead). Lists and nested maps are skipped. + """ + match = FRONTMATTER_RE.match(text) + if not match: + return {}, text + data: dict[str, str] = {} + current_key: str | None = None + for line in match.group(1).splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + if line.startswith(" ") or line.startswith("\t"): + current_key = None + continue + if ":" not in line: + continue + key, _, value = line.partition(":") + key = key.strip() + value = value.strip().strip('"').strip("'") + current_key = key + data[key] = value + body = text[match.end():] + return data, body + + +def page_for(rel_path: str, fallback: str | None = None) -> Page | None: + src = DOCS_DIR / rel_path + if not src.is_file(): + print(f"warning: missing source page {rel_path}", file=sys.stderr) + return None + text = src.read_text(encoding="utf-8") + fm, _ = parse_frontmatter(text) + title = fm.get("title") or fallback or fallback_title(rel_path) + description = fm.get("description") or fm.get("lead", "") + return Page(rel_path=rel_path, title=title, description=description) + + +def fallback_title(rel_path: str) -> str: + stem = Path(rel_path).stem + if stem == "index": + stem = Path(rel_path).parent.name or "Home" + return stem.replace("-", " ").replace("_", " ").title() + + +def md_url(rel_path: str, base_url: str) -> str: + return f"{base_url.rstrip('/')}/{rel_path}" + + +def copy_source_markdown(rel_paths: Iterable[str]) -> int: + count = 0 + for rel_path in rel_paths: + src = DOCS_DIR / rel_path + dst = SITE_DIR / rel_path + if not src.is_file(): + continue + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + count += 1 + return count + + +def collect_nav_pages( + nav: list, prefix: tuple[str, ...] = () +) -> list[tuple[tuple[str, ...], str, str]]: + """Walk the nav structure and yield (section_path, page_rel_path, label) tuples.""" + pages: list[tuple[tuple[str, ...], str, str]] = [] + for entry in nav: + if not isinstance(entry, dict) or len(entry) != 1: + continue + label, value = next(iter(entry.items())) + if isinstance(value, str): + pages.append((prefix, value, label)) + elif isinstance(value, list): + pages.extend(collect_nav_pages(value, prefix + (label,))) + return pages + + +def all_source_pages() -> list[str]: + return sorted( + str(p.relative_to(DOCS_DIR)).replace("\\", "/") + for p in DOCS_DIR.rglob("*.md") + ) + + +def render_llms_txt(config: dict, base_url: str) -> str: + project = config.get("project", {}) + site_name = project.get("site_name", "Documentation") + site_description = project.get("site_description", "") + nav = project.get("nav", []) + + nav_pages = collect_nav_pages(nav) + seen: set[str] = set() + + lines: list[str] = [f"# {site_name}", ""] + if site_description: + lines += [f"> {site_description}", ""] + lines += [ + "Every documentation page is also available as Markdown by visiting", + "the same URL with a `.md` suffix (for example `/articles/insights.md`).", + "A single concatenated copy of all pages lives at `/llms-full.txt`.", + "", + ] + + sections: dict[tuple[str, ...], list[Page]] = {} + for section_path, rel_path, label in nav_pages: + if rel_path in seen: + continue + seen.add(rel_path) + page = page_for(rel_path, fallback=label) + if page is None: + continue + sections.setdefault(section_path, []).append(page) + + for section_path, pages in sections.items(): + heading = " / ".join(section_path) if section_path else "Overview" + lines.append(f"## {heading}") + lines.append("") + for page in pages: + url = md_url(page.rel_path, base_url) + entry = f"- [{page.title}]({url})" + if page.description: + entry += f": {page.description}" + lines.append(entry) + lines.append("") + + extras = sorted( + rel_path for rel_path in all_source_pages() if rel_path not in seen + ) + if extras: + lines.append("## Optional") + lines.append("") + for rel_path in extras: + page = page_for(rel_path) + if page is None: + continue + url = md_url(rel_path, base_url) + entry = f"- [{page.title}]({url})" + if page.description: + entry += f": {page.description}" + lines.append(entry) + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +def render_llms_full_txt(config: dict, base_url: str) -> str: + project = config.get("project", {}) + site_name = project.get("site_name", "Documentation") + + parts: list[str] = [ + f"# {site_name} (full text)", + "", + "Concatenated source for every documentation page. Pages are separated", + "by horizontal rules and labelled with their canonical URL.", + "", + ] + + for rel_path in all_source_pages(): + page = page_for(rel_path) + if page is None: + continue + url = md_url(rel_path, base_url) + text = (DOCS_DIR / rel_path).read_text(encoding="utf-8") + parts += [ + "---", + "", + f"# {page.title}", + "", + f"Source: {url}", + "", + text.rstrip(), + "", + ] + + return "\n".join(parts).rstrip() + "\n" + + +def main() -> int: + if not SITE_DIR.is_dir(): + print(f"error: site directory not found at {SITE_DIR}", file=sys.stderr) + print("run `zensical build` before this script", file=sys.stderr) + return 1 + + config = load_config() + site_url = config.get("project", {}).get("site_url") or DEFAULT_SITE_URL + base_url = site_url.rstrip("/") + + rel_paths = all_source_pages() + copied = copy_source_markdown(rel_paths) + print(f"copied {copied} markdown sources into {SITE_DIR}") + + if STATIC_WEB_APP_CONFIG.is_file(): + shutil.copy2(STATIC_WEB_APP_CONFIG, SITE_DIR / STATIC_WEB_APP_CONFIG.name) + print(f"copied {STATIC_WEB_APP_CONFIG.name} into {SITE_DIR}") + + llms_txt = render_llms_txt(config, base_url) + (SITE_DIR / "llms.txt").write_text(llms_txt, encoding="utf-8") + print(f"wrote {SITE_DIR / 'llms.txt'}") + + llms_full_txt = render_llms_full_txt(config, base_url) + (SITE_DIR / "llms-full.txt").write_text(llms_full_txt, encoding="utf-8") + print(f"wrote {SITE_DIR / 'llms-full.txt'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/staticwebapp.config.json b/staticwebapp.config.json new file mode 100644 index 0000000..d69301a --- /dev/null +++ b/staticwebapp.config.json @@ -0,0 +1,6 @@ +{ + "mimeTypes": { + ".md": "text/markdown; charset=utf-8", + ".txt": "text/plain; charset=utf-8" + } +} From f8c5caafc7b49a9c14a82486e8e21a9b1e5d3b53 Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Mon, 25 May 2026 12:37:36 +0200 Subject: [PATCH 16/16] feat: document v3 sdk --- docs/articles/duration-signals.md | 197 +++++--------- docs/articles/navigation-signals.md | 69 ++--- docs/articles/preset-errors.md | 128 ++++----- docs/articles/preset-purchases.md | 109 ++++---- docs/articles/swift-custom-processors.md | 242 +++++++++++++++++ docs/articles/swift-processors.md | 188 +++++++++++++ docs/articles/swift-user-identification.md | 87 ++++++ docs/articles/telemetry-client.md | 115 ++------ docs/articles/test-mode.md | 90 +++---- docs/guides/objective-c-setup.md | 146 +++------- docs/guides/swift-migration-v3.md | 210 +++++++++++++++ docs/guides/swift-setup.md | 298 +++++++++------------ docs/integrations/revenuecat.md | 102 ++++--- docs/integrations/superwall.md | 43 ++- zensical.toml | 8 +- 15 files changed, 1230 insertions(+), 802 deletions(-) create mode 100644 docs/articles/swift-custom-processors.md create mode 100644 docs/articles/swift-processors.md create mode 100644 docs/articles/swift-user-identification.md create mode 100644 docs/guides/swift-migration-v3.md diff --git a/docs/articles/duration-signals.md b/docs/articles/duration-signals.md index 6ec2bbf..595f18f 100644 --- a/docs/articles/duration-signals.md +++ b/docs/articles/duration-signals.md @@ -1,45 +1,32 @@ --- -title: Track time-based user engagement with Duration Signals +title: Duration Events tags: - setup - how-to - beginner - swift -description: Duration Signals allow you to measure how long users spend on specific activities in your app with millisecond precision. -lead: With Duration Signals, you can easily measure how long users spend on different activities in your app, helping you identify engagement patterns, optimize workflows, and improve user experience with precise timing data. -searchEngineTitle: Track User Engagement with Duration Signals in TelemetryDeck -searchEngineDescription: Learn how to implement and analyze time-based metrics in your app using TelemetryDeck's Duration Signals. +description: Duration events let you measure how long users spend on specific activities in your app. +lead: Measure how long users spend on different activities in your app — onboarding steps, content consumption, checkout flows, or any user journey. +searchEngineTitle: Track User Engagement with Duration Events in TelemetryDeck +searchEngineDescription: Learn how to implement and analyze time-based metrics in your app using TelemetryDeck's duration events. +testedOn: SwiftSDK 3.0.0 --- -## What are Duration Signals? +## How it works -Duration Signals are a powerful feature of TelemetryDeck's SDKs that make it easier than ever to understand how users interact with your app over time. Whether you want to track time spent during onboarding, content consumption, checkout flows, or any other user journey, Duration Signals provide accurate, millisecond-precise timing data. - -The SDK automatically handles all the complexities of time tracking for you: -- Precise measurement down to milliseconds (3 decimal places) -- Automatic exclusion of time spent while the app is in the background -- Merging parameters from both start and stop calls -- Thread-safe implementation for accurate timing - -## Implementation - -Using Duration Signals is as simple as bracketing an activity with two function calls: +Bracket an activity with two calls — start and stop. The SDK handles timing, background exclusion, and parameter merging automatically. ```swift -// Start tracking when the activity begins -TelemetryDeck.startDurationSignal("activityName") +await TelemetryDeck.startDurationEvent("activityName") // ... user performs the activity ... -// Stop tracking and send the signal when the activity ends -TelemetryDeck.stopAndSendDurationSignal("activityName") +await TelemetryDeck.stopAndSendDurationEvent("activityName") ``` -The duration is automatically calculated and included in your signal as `TelemetryDeck.Signal.durationInSeconds`. +The duration is included as `TelemetryDeck.Signal.durationInSeconds` with millisecond precision (3 decimal places). -### View Lifecycle Integration - -Duration Signals integrate seamlessly with your view lifecycles in SwiftUI: +### SwiftUI view lifecycle ```swift struct TutorialView: View { @@ -48,45 +35,56 @@ struct TutorialView: View { Text("Welcome to the Tutorial!") } .onAppear { - TelemetryDeck.startDurationSignal("tutorial") + Task { await TelemetryDeck.startDurationEvent("tutorial") } } .onDisappear { - TelemetryDeck.stopAndSendDurationSignal("tutorial") + Task { await TelemetryDeck.stopAndSendDurationEvent("tutorial") } } } } ``` -Both functions also take an optional `parameters` argument where you can pass additional information just like with the `signal` function. +Both functions accept optional `parameters` for additional context. + +## SDK requirements -## Technical Details +- Swift SDK: 3.0.0 or later +- Kotlin SDK: 4.1.0 or later +- Flutter SDK: 2.1.0 or later -### SDK Requirements +## Edge cases -- Swift SDK: Version 2.7.0 or later -- Kotlin SDK: Version 4.1.0 or later -- Flutter SDK: Version 2.1.0 or later +- **Multiple starts**: Calling `startDurationEvent` with an already-tracked name discards the previous tracking and starts fresh. +- **Missing stop**: A duration event that's never stopped is never sent. +- **App restarts**: Duration events survive app restarts via persistent storage. +- **Background time**: Excluded by default. Pass `includeBackgroundTime: true` to include it: -### Edge Cases & Limitations +```swift +await TelemetryDeck.startDurationEvent( + "longRunningTask", + includeBackgroundTime: true +) +``` + +### Cancelling a duration event -- **Multiple starts**: If you call `startDurationSignal` with a name that's already being tracked, the previous tracking is discarded and a new one begins. -- **Missing stop**: If a duration signal is never stopped, it will not be sent. -- **Signal name conflicts**: Use unique signal names for different activities to avoid conflicts. -- **App restarts**: Duration signals are not stored to persistent storage, therefore they are not appropriate for tracking long-term user engagement. +If the activity is abandoned before completion: -## Analyzing Duration Data +```swift +await TelemetryDeck.cancelDurationEvent("activityName") +``` -Duration data is sent as a numerical value in the `TelemetryDeck.Signal.durationInSeconds` parameter, which opens up several analysis possibilities. +## Analyzing duration data -### Using the Histogram Aggregation +Duration data is sent as a numerical value in the `TelemetryDeck.Signal.durationInSeconds` parameter. The histogram aggregation is a natural fit for visualizing distribution. -The histogram aggregation type is perfect for visualizing the distribution of duration data: +### Histogram query 1. Create a new insight of type "Advanced Query", then open the "JSON Editor": ![A screenshot of the Query Creator dialog](/assets/duration-signal-01.png) -2. Copy & paste the following histogram aggregation query and adjust `` to your needs: +2. Paste this histogram aggregation query, replacing ``: ![A screenshot of the JSON Editor text field](/assets/duration-signal-02.png) @@ -114,7 +112,7 @@ The histogram aggregation type is perfect for visualizing the distribution of du { "dimension": "type", "type": "selector", - "value": "" + "value": "" } ] }, @@ -123,128 +121,69 @@ The histogram aggregation type is perfect for visualizing the distribution of du } ``` -3. Set the chart type to be a bar chart in the UI via the insight's top right segmented control: +3. Set the chart type to bar chart: ![A screenshot of the insight set to be a bar chart](/assets/duration-signal-03.png) -4. You might also want to adjust the `splitPoints` array based on the expected duration of your activity, for example: +4. Adjust `splitPoints` to match your expected durations: - **Short interactions** (button clicks): `[0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1, 2, 5]` - **Medium interactions** (form fills): `[0, 1, 2, 3, 4, 5, 7.5, 10, 15, 20, 30]` - **Long interactions** (content consumption): `[0, 5, 15, 30, 60, 120, 300, 600, 1200]` -## Common Use Cases +## Examples -### Onboarding Optimization - -Track time spent in each step of your onboarding flow to identify which steps take too long or where users might get stuck: +### Onboarding steps ```swift -// In first onboarding screen -TelemetryDeck.startDurationSignal("Onboarding.step1") - -// When moving to second screen -TelemetryDeck.stopAndSendDurationSignal("Onboarding.step1", parameters: ["pushAccess": "granted"]) -TelemetryDeck.startDurationSignal("Onboarding.step2") +await TelemetryDeck.startDurationEvent("Onboarding.step1") -// etc. +// When moving to step 2 +await TelemetryDeck.stopAndSendDurationEvent("Onboarding.step1", parameters: [ + "pushAccess": "granted" +]) +await TelemetryDeck.startDurationEvent("Onboarding.step2") ``` -Note that Duration Signals are just ordinary signals, so you can totally reuse these for creating [funnel charts](https://telemetrydeck.com/docs/articles/how-to-funnel-insights/) and more. +Duration events are regular events, so you can reuse them in [funnel charts](/articles/how-to-funnel-insights/). -### Content Engagement - -Measure how long users engage with different content types to understand what resonates with your audience: +### Content engagement ```swift -// When user opens an article -TelemetryDeck.startDurationSignal("Content.viewing", parameters: [ +await TelemetryDeck.startDurationEvent("Content.viewing", parameters: [ "contentType": "article", "contentID": article.id, "contentCategory": article.category, ]) -// When user leaves the article -TelemetryDeck.stopAndSendDurationSignal("Content.viewing", parameters: [ - "reachedEnd": userReachedEnd ? "true" : "false", -]) -``` - -### Feature Discovery - -Track how long users spend exploring new features to assess the effectiveness of your feature introduction: - -```swift -// When user enters new feature area -TelemetryDeck.startDurationSignal("Feature.exploration", parameters: [ - "featureName": "videoEditor", - "entryPoint": entryPoint, -]) - -// When user leaves the feature area -TelemetryDeck.stopAndSendDurationSignal("Feature.exploration", parameters: [ - "completedAction": userCreatedVideo ? "true" : "false" -]) -``` - -### Performance Monitoring - -Track real-world performance metrics by measuring operation durations: - -```swift -// Before starting an intensive operation -TelemetryDeck.startDurationSignal("Render.operation", parameters: [ - "complexity": "\(complexity)", - "inputSize": "\(inputSizeInMB)", -]) - -// After operation completes -TelemetryDeck.stopAndSendDurationSignal("Render.operation", parameters: [ - "success": success ? "true" : "false", - "outputSize": "\(outputSizeInMB)", +// When leaving the article +await TelemetryDeck.stopAndSendDurationEvent("Content.viewing", parameters: [ + "reachedEnd": userReachedEnd ]) ``` -### Network Request Timing - -Duration Signals methods are marked with `@MainActor`, which means two things: - -1. In UI contexts like SwiftUI views, no `await` is needed (as shown in the above examples) -2. When calling from background contexts like network operations, you need to use `await` - -Here's how to measure network requests that run on background threads: +### Network request timing ```swift func fetchData() async throws -> Data { - // Since we're potentially on a background thread, await is needed - await TelemetryDeck.startDurationSignal("Network.fetch", parameters: [ + await TelemetryDeck.startDurationEvent("Network.fetch", parameters: [ "endpoint": "users/profile" ]) - + do { - // Perform your network request let (data, response) = try await URLSession.shared.data(from: url) let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - - // Again, await is needed here - await TelemetryDeck.stopAndSendDurationSignal("Network.fetch", parameters: [ - "status": "\(statusCode)", - "success": "true" + + await TelemetryDeck.stopAndSendDurationEvent("Network.fetch", parameters: [ + "status": statusCode, + "success": true ]) - + return data } catch { - await TelemetryDeck.stopAndSendDurationSignal("Network.fetch", parameters: [ - "success": "false" + await TelemetryDeck.stopAndSendDurationEvent("Network.fetch", parameters: [ + "success": false ]) throw error } } ``` - -## What's Next? - -Start by identifying a few key user journeys or critical performance areas in your app that would benefit from timing data. Implement Duration Signals for these activities first, then use the histogram aggregation to visualize and analyze the results. - -Remember that Duration Signals can be combined with your existing analytics strategy - they provide an additional dimension to your user data without replacing what you already have. - -{% callToAction "Explore more analytics possibilities" "Track user engagement and make data-driven decisions" %} \ No newline at end of file diff --git a/docs/articles/navigation-signals.md b/docs/articles/navigation-signals.md index 4692b06..6b6e182 100644 --- a/docs/articles/navigation-signals.md +++ b/docs/articles/navigation-signals.md @@ -1,24 +1,18 @@ --- title: Navigation Signals tags: setup -description: TelemetryDeck can track how users navigate through your app when you send navigation signals. Here's how these need to look like. -lead: TelemetryDeck can track how users navigate through your app when you send navigation signals. Here's how these need to look like. -testedOn: SwiftSDK 2.2.0, WebSDK 1.0.0 +description: TelemetryDeck tracks how users navigate through your app when you send navigation signals. +lead: TelemetryDeck tracks how users navigate through your app when you send navigation signals. Here's the format and convenience methods. +testedOn: SwiftSDK 3.0.0, WebSDK 1.0.0 --- -{% notewarning "Upcoming Feature" %} +!!! note "Web Analytics already tracks navigation" -This feature is still in development and will take a while to be available in all SDKs and the Dashboard UI. We encourage you to start sending navigation signals now so you'll have data to play around with once we launch the feature fully. -{% endnotewarning %} - -{% noteinfo "Web Analytics already tracks navigation" %} - -If you're using TelemetryDeck's Web SDK to track your website, you don't need to send navigation signals. The Web SDK already tracks navigation automatically. -{% endnoteinfo %} + If you're using TelemetryDeck's Web SDK, navigation is tracked automatically. This guide is for native app SDKs. ## Format -A navigation signal is a regular TelemetryDeck signal of type `TelemetryDeck.Navigation.pathChanged`. It has parameters for version number, source and destination paths, and an identifier, which is the source path and destination path concatenated with `->`. Using this identifier, we can track how users navigate through your app. +A navigation signal has the type `TelemetryDeck.Navigation.pathChanged` with these parameters: ```json { @@ -34,24 +28,16 @@ A navigation signal is a regular TelemetryDeck signal of type `TelemetryDeck.Nav } ``` -Values in angle brackets (`< >`) are placeholders and should be replaced with actual values. - -The signal type should always be `TelemetryDeck.Navigation.pathChanged` for navigation signals. +| Key | Description | +|---|---| +| `TelemetryDeck.Navigation.schemaVersion` | Always `"1"`. | +| `TelemetryDeck.Navigation.identifier` | ` -> `. Used to build directed navigation graphs. | +| `TelemetryDeck.Navigation.sourcePath` | Where the user came from. | +| `TelemetryDeck.Navigation.destinationPath` | Where the user went. | -Here's what each parameter should contain: +## Navigation paths -| Key | Description | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `TelemetryDeck.Navigation.schemaVersion` | The version of the schema. Should always be `"1"`. We'll use this to account for if we ever need to update this schema. | -| `TelemetryDeck.Navigation.identifier` | The source navigation path, followed by `->`, followed by the destination navigation path. This is the most important part of the navigation schema, we'll use this to build directed graphs. | -| `TelemetryDeck.Navigation.sourcePath` | A navigation path that describes the source of the navigation. | -| `TelemetryDeck.Navigation.destinationPath` | A navigation that describes the destination of the navigation. | - -## Navigation Paths - -Navigation Paths are strings that describe a location or view in your application or website. They must be -delineated by `.` characters in apps and `/` in websites. Delineation characters at the beginning and end of the string are -ignored. Use the empty string `""` for navigation from outside the app. +Navigation paths are `.`-delimited strings in apps and `/`-delimited in websites. Leading and trailing delimiters are ignored. Use the empty string `""` for navigation from outside the app. Examples: @@ -59,26 +45,23 @@ Examples: - `settings.user.changePassword` - `/blog/ios-market-share` -## Automatic Navigation Tracking - -Since TelemetryDeck navigation signals are slightly cumbersome to create manually, we're aiming to provide convenience methods for our SDKs that will automatically track navigation signals for you. These methods will be in one of two flavors, either a method that you call with a source and destination, or a method that you call with just a destination. +## Convenience methods -### `TelemetryDeck.navigationPathChanged(from: , to: )` +### With source and destination -Calling this method will automatically create a navigation signal with the given source and destination. - -### `TelemetryDeck.navigationPathChanged(to: )` +```swift +await TelemetryDeck.navigationPathChanged(from: "home", to: "settings") +``` -Calling this method with just a destination will use the previously last used source as the source for the navigation signal. +### Destination only -This is convenient, but might produce incorrect graphs if you don't call it from every screen in your app. -Suppose you have 3 tabs "Home", "User" and "Settings", but only set up navigation in "Home" and "Settings". If -a user taps "Home", "User" and "Settings" in that order, that'll produce an incorrect navigation signal with -source "Home" and destination "Settings", a path that the user did not take. +```swift +await TelemetryDeck.navigationPathChanged(to: "settings") +``` -#### SwiftUI Convenience +This uses the previous destination as the source. Be careful — if you don't call it from every screen, the navigation graph will show paths the user didn't take. -If you're using SwiftUI, you can use the `.trackNavigation(path:)` view modifier as a convenient wrapper around `navigationPathChanged(to:)`. It will automatically send the navigation signal when the view appears: +### SwiftUI view modifier ```swift struct SettingsView: View { @@ -94,4 +77,4 @@ struct SettingsView: View { } ``` -The same considerations about consistent application apply - make sure to use the modifier on all navigation destinations to avoid incorrect navigation paths. +The `.trackNavigation(path:)` modifier calls `navigationPathChanged(to:)` when the view appears. Apply it consistently to all navigation destinations to get accurate graphs. diff --git a/docs/articles/preset-errors.md b/docs/articles/preset-errors.md index 05c71e8..2f94ff0 100644 --- a/docs/articles/preset-errors.md +++ b/docs/articles/preset-errors.md @@ -1,106 +1,94 @@ --- -title: Setting Up the 'Errors' Preset to Reduce Churn +title: Tracking Errors tags: - setup - beginner - insights - presets -lead: TelemetryDeck ships with a set of insights that can be useful to learn what kind of issues your users encounter most in your apps. Here's how to set them up. +lead: Track errors in your app to identify common issues and reduce churn. The TelemetryDeck SDK provides convenience methods for structured error reporting. --- -## Why Track Errors? +## Why track errors? -Success is often defined in numbers such as Monthly Active Users (MAU) or Monthly Recurring Revenue (MRR). Churn is a metric that tracks the percentage of active/paying users who stopped using/paying for your app. - -A smooth experience without bugs & issues is one of the key factors contributing to a lower churn rate. If you do a good job in continuously fixing the most common issues with your product, you're on the right track for success. That's why it's important that you detect any common issues your users get stuck at while using your app. - -{% noteinfo "What Error Handling Really Is" %} -Error Handling is any logic that detects that something unexpected happened and reacts to that information in some useful way. It's commonly interpreted as 'showing an error message to a user', but that's just the most basic form of Error Handling. Other common things you can do to improve your app are Empty States (Why is this empty?), Call-to-Actions (What can I do?), and Auto-Repair (resiliency against common unexpected user input). But always make sure to give some form of feedback instead of failing silently, which makes your app feel broken. -{% endnoteinfo %} +A smooth experience directly impacts retention. Tracking errors lets you find the most common issues your users hit, prioritize fixes, and measure improvement over time. ## Using the TelemetryDeck Swift SDK -If you're using the TelemetryDeck Swift SDK, tracking errors is simple. Use the convenience method we've prepared: - ```swift -TelemetryDeck.errorOccurred(id: error.localizedDescription) +await TelemetryDeck.errorOccurred(id: error.localizedDescription) ``` -### Better Error Identification +### Better error identification -While `error.localizedDescription` works, it's not optimal because the same error will be reported differently based on user language settings. For better error grouping, provide a consistent ID: +`error.localizedDescription` varies by user language, making it hard to group the same error. Provide a consistent ID instead: ```swift do { - let object = try JSONDecoder().decode(Object.self, from: data) + let object = try JSONDecoder().decode(Object.self, from: data) } catch { - // Option 1: Using error.with(id:) - TelemetryDeck.errorOccurred( - identifiableError: error.with(id: "ImportObject.jsonDecode") - ) - - // Option 2: Using explicit parameters - TelemetryDeck.errorOccurred( - id: "ImportObject.jsonDecode", - message: error.localizedDescription - ) + await TelemetryDeck.errorOccurred( + identifiableError: error.with(id: "ImportObject.jsonDecode") + ) + + // Or with explicit parameters: + await TelemetryDeck.errorOccurred( + id: "ImportObject.jsonDecode", + message: error.localizedDescription + ) } ``` -### Custom Error Types +### Custom error types -For your own error types, conform to the `IdentifiableError` protocol (built into the Swift SDK): +Conform your own error types to `IdentifiableError`: ```swift enum MyError: String, IdentifiableError { - case fileMissing - case invalidFormat + case fileMissing + case invalidFormat - var id: String { self.rawValue } + var id: String { self.rawValue } } -// Then report directly: -TelemetryDeck.errorOccurred(identifiableError: myError) +await TelemetryDeck.errorOccurred(identifiableError: myError) ``` -### Error Categories +### Error categories -The Swift SDK provides three built-in error categories for better organization: +Three built-in categories for organization: ```swift -// Thrown exceptions (parsing errors, I/O errors, permission errors) -TelemetryDeck.errorOccurred( - id: "FileNotFound", - category: .thrownException, - message: error.localizedDescription +// Thrown exceptions — parsing errors, I/O errors, permission errors +await TelemetryDeck.errorOccurred( + id: "FileNotFound", + category: .thrownException, + message: error.localizedDescription ) -// User input errors (invalid format, invalid range) -TelemetryDeck.errorOccurred( - id: "ProjectForm.hourlyRateConversionFailed", - category: .userInput, - message: "Text '\(self.textFieldInput)' could not be converted to type 'Int'." +// User input errors — invalid format, invalid range +await TelemetryDeck.errorOccurred( + id: "ProjectForm.hourlyRateConversionFailed", + category: .userInput, + message: "Text could not be converted to Int" ) -// App state errors (inconsistent navigation, invalid combinations) -TelemetryDeck.errorOccurred( - id: "NavigationState.invalidTransition", - category: .appState, - message: "Cannot navigate from login to dashboard without authentication" +// App state errors — inconsistent navigation, invalid combinations +await TelemetryDeck.errorOccurred( + id: "NavigationState.invalidTransition", + category: .appState, + message: "Cannot navigate from login to dashboard without authentication" ) ``` -{% noteinfo "Default Category" %} -When using `TelemetryDeck.errorOccurred(identifiableError:)`, the category defaults to `.thrownException`, but you can override it if needed. -{% endnoteinfo %} +!!! note -The `errorOccurred` function accepts the same optional arguments as the `signal` function (namely `parameters`, `floatValue`, `customUserID`) in case you want to provide additional context info. + When using `errorOccurred(identifiableError:)`, the category defaults to `.thrownException`. -## Manual Signal Construction for Other Platforms +The `errorOccurred` function accepts the same optional arguments as `event` (`parameters`, `floatValue`, `customUserID`). -If you're not using the TelemetryDeck Swift SDK (for example, on Android, Web, or other platforms), you'll need to manually build the error signal. +## Manual signal construction for other platforms -### Signal Structure +If you're not using the Swift SDK, construct the error signal manually. **Event name**: `TelemetryDeck.Error.occurred` @@ -110,25 +98,19 @@ If you're not using the TelemetryDeck Swift SDK (for example, on Android, Web, o **Optional parameters**: -- `TelemetryDeck.Error.message`: The full error message or description +- `TelemetryDeck.Error.message`: The full error message - `TelemetryDeck.Error.category`: One of `thrown-exception`, `user-input`, or `app-state` -## Built-In Error Categories - -Unexpected behavior generally falls into one of three categories, each with a dedicated chart in the "Errors" tab: - -1. **Thrown Exceptions** (`thrown-exception`): Parsing errors, I/O errors, permission errors -2. **User Input** (`user-input`): Invalid text format, invalid number format, invalid date range -3. **App State** (`app-state`): Inconsistent navigation request, invalid combination of form options - -### When to Use Each Category +## Built-in error categories -- **thrown-exception**: Use in `try-catch` blocks or when handling `null`/`nil` returns from operations that might fail -- **user-input**: Use when converting or validating user input with fallback behavior -- **app-state**: Use for assertion failures or unexpected application states that shouldn't occur in normal operation +| Category | Use when | +|---|---| +| `thrown-exception` | In `try-catch` blocks or when handling `nil` returns from operations that might fail | +| `user-input` | Validating or converting user input with fallback behavior | +| `app-state` | Assertion failures or unexpected application states | -## Effect on Privacy & App Tracking Transparency +## Privacy considerations -If you are sending dynamic values such as `error.localizedDescription` or if any of the parameter fields contain user-dynamic data such as file paths or input data, some user data might be sent to TelemetryDeck. It really depends on the nature of this data and how you plan to use it that influences what fields in App Tracking Transparency you need to add. You might need to adjust your privacy report accordingly. +If you send dynamic values like `error.localizedDescription` or user-generated content in parameters, some user data reaches TelemetryDeck. Adjust your App Tracking Transparency disclosure accordingly. -TelemetryDeck does not attempt to link collected data to the users identity, nor do we use data for tracking purposes. To protect your users privacy, we urge you to not send any data that might identify your users. +TelemetryDeck does not link collected data to user identity or use it for tracking. Don't send data that could identify your users. diff --git a/docs/articles/preset-purchases.md b/docs/articles/preset-purchases.md index 9f2256f..5737c08 100644 --- a/docs/articles/preset-purchases.md +++ b/docs/articles/preset-purchases.md @@ -1,112 +1,103 @@ --- -title: Setting Up the 'Purchases' Preset to Get Live Purchase Data +title: Tracking Purchases tags: - setup - beginner - insights - presets -lead: TelemetryDeck ships with a set of insights that can be useful to track your revenue within the last few hours with live purchase data. Here's how to set them up. -searchEngineDescription: TelemetryDeck ships with a set of insights that can be useful to track your revenue within the last few hours with live purchase data. Learn how to set them up. +lead: Track in-app purchases through TelemetryDeck for near-realtime revenue data — no waiting hours for App Store Connect to update. +searchEngineDescription: Track your app's in-app purchases through TelemetryDeck with just a couple of seconds delay, providing live revenue data. +testedOn: SwiftSDK 3.0.0 --- -## Why Track Purchases? +## Why track purchases? -If you are offering In-App Purchases in your app, you might have noticed some delay in officially reported purchase stats. For example, App Store Connect charts do not offer any purchase data for the last 3 hours. Such a delay can be annoying sometimes such as at the day of your app launch or a specific live event related to your app. On top of that, App Store Connect in particular signs you out of your account regularly, making it annoying to quickly look up purchase statistics. +App Store Connect purchase data lags by several hours and requires frequent re-authentication. TelemetryDeck gives you purchase data within seconds. -That's why you might want to set up a signal in your application to track purchases in your app through TelemetryDeck with just a couple of seconds delay, providing you with the live data you want. +You can track purchases through: -You can use these methods to include your purchase data in TelemetryDeck: +- The TelemetryDeck Swift SDK directly +- RevenueCat integration +- FreemiumKit integration -- Use the TelemetryDeck Swift SDK directly -- If you're already using RevenueCat, you can use the RevenueCat Integration -- If you're using FreemiumKit, you can connect that to TelemetryDeck +!!! warning "Live data vs. correct data" -See the sections below for a detailed description. - -{% notewarning "Live Data vs. Correct Data" %} -We do not offer any intelligence to correct once reported purchases, such as when users make refunds, or to detect subscription renewals. Therefore, our insights focus on more recent data. For longer-term or 100% correct data, refer to official sources. -{% endnotewarning %} + TelemetryDeck does not handle refunds or detect subscription renewals. For long-term or 100% correct revenue data, use official sources like App Store Connect. ## Using the TelemetryDeck Swift SDK -If you're using the TelemetryDeck Swift SDK, tracking purchases is incredibly simple. Just call the convenience method when you receive a StoreKit transaction: +Pass a StoreKit transaction to the convenience method: ```swift -TelemetryDeck.purchaseCompleted(transaction: transaction) +await TelemetryDeck.purchaseCompleted(transaction: transaction) ``` -That's it! This method automatically: +This automatically: - Extracts the price from the transaction -- Converts the currency to USD (using hard-coded exchange rates) +- Converts the currency to USD (using built-in exchange rates) - Determines if it's a subscription or one-time purchase +- Detects free trial starts vs. paid conversions - Includes the storefront country and currency codes -- Sends the properly formatted signal to TelemetryDeck -{% noteinfo "Requirements" %} -The `purchaseCompleted` convenience function is only available on iOS 15 or higher. It accepts the same optional arguments as the `signal` function (namely `parameters` and `customUserID`) in case you want to provide additional context info. -{% endnoteinfo %} +!!! note "Requirements" + + Requires iOS 15+. Accepts optional `parameters` and `customUserID` for additional context. + +### Automatic trial conversion detection + +The SDK includes a `TrialConversionProcessor` that monitors StoreKit `Transaction.updates` in the background. When a user transitions from a free trial to a paid subscription, it automatically fires `TelemetryDeck.Purchase.convertedFromTrial`. No additional code needed. ## Using TelemetryDeck with RevenueCat -If you use [RevenueCat](https://revenuecat.com), you can use our [RevenueCat Setup Guide](/docs/integrations/revenuecat/). +See our [RevenueCat Setup Guide](/integrations/revenuecat/). ## Using FreemiumKit -If you use [FreemiumKit](https://freemiumkit.app), just add their SDKs `.onPurchaseCompleted` view modifier to your main view. It passes the `transaction` parameter to the closure, which you can directly pass to `TelemetryDeck.purchaseCompleted(transaction: transaction)`. Read the related section in their [setup guide](https://freemiumkit.app/documentation/freemiumkit/setupguide#Direct-Access-to-StoreKit-Transactions) to learn more. +Add FreemiumKit's `.onPurchaseCompleted` view modifier to your main view — it passes the `transaction` parameter directly to `TelemetryDeck.purchaseCompleted(transaction:)`. See their [setup guide](https://freemiumkit.app/documentation/freemiumkit/setupguide#Direct-Access-to-StoreKit-Transactions). -## Manual Signal Structure for Other Platforms +## Manual signal construction for other platforms -{% notewarning "Only Needed for Non-Swift Platforms" %} -The following section describes the manual signal structure only necessary if you are NOT using the TelemetryDeck Swift SDK. Swift developers should use the `purchaseCompleted` convenience method described above. -{% endnotewarning %} +!!! warning -If you're reporting purchases from other platforms (Android, Web, etc.), you'll need to manually construct and send the purchase signal with the following structure: + Only needed if you are NOT using the TelemetryDeck Swift SDK. -### Required Fields +### Required fields -- **Event name**: Must be `TelemetryDeck.Purchase.completed` +- **Event name**: `TelemetryDeck.Purchase.completed` - **`floatValue`**: The purchase amount in USD -{% notewarning "Manual Currency Conversion Required" %} -When sending purchase signals manually, you MUST convert the transaction value to USD yourself before sending. You can use [an API like this](https://www.exchangerate-api.com/docs/standard-requests) which offers 1,500 requests per month free of charge to get current exchange rates. Alternatively, you could fetch & hard-code exchange rates in your app for a rough estimate if you expect more than 1,500 purchases per month. -{% endnotewarning %} +!!! warning "Currency conversion" -### Optional but Recommended Payload Keys + You must convert the transaction value to USD before sending. Use [an exchange rate API](https://www.exchangerate-api.com/docs/standard-requests) (1,500 free requests/month) or hard-code approximate rates. -To get more detailed insights, include these additional parameters: +### Optional parameters -- `TelemetryDeck.Purchase.type`: Either `subscription` or `one-time-purchase` -- `TelemetryDeck.Purchase.countryCode`: The country code of the storefront -- `TelemetryDeck.Purchase.currencyCode`: The currency code of the storefront +- `TelemetryDeck.Purchase.type`: `subscription` or `one-time-purchase` +- `TelemetryDeck.Purchase.countryCode`: Storefront country code +- `TelemetryDeck.Purchase.currencyCode`: Storefront currency code -### Example Manual Implementation (Swift) - -Here's what the manual implementation looks like if you need to customize it or understand what the convenience method does internally: +### Example ```swift -// Convert price to USD first (you need to handle currency conversion) let priceInUSD = convertToUSD(transaction.price, from: transaction.currencyCode) -TelemetryDeck.signal( - "TelemetryDeck.Purchase.completed", - parameters: [ - "TelemetryDeck.Purchase.type": transaction.subscriptionGroupID != nil ? "subscription" : "one-time-purchase", - "TelemetryDeck.Purchase.countryCode": transaction.storefrontCountryCode, - "TelemetryDeck.Purchase.currencyCode": transaction.currencyCode ?? "???" - ], - floatValue: priceInUSD +await TelemetryDeck.event( + "TelemetryDeck.Purchase.completed", + parameters: [ + "TelemetryDeck.Purchase.type": transaction.subscriptionGroupID != nil + ? "subscription" : "one-time-purchase", + "TelemetryDeck.Purchase.countryCode": transaction.storefrontCountryCode, + "TelemetryDeck.Purchase.currencyCode": transaction.currencyCode ?? "???" + ], + floatValue: priceInUSD ) ``` -## Effect on Privacy & App Tracking Transparency - -If you are using a 3rd-party service like RevenueCat, you don't need to change your privacy labels at all because you're sending way less data to TelemetryDeck than you are already to those services. So if you've followed their guides, you should be good. - -If you aren't using a 3rd-party library, you are now sending purchase history data to TelemetryDeck. So make sure to mark the checkbox for "Analytics" in the "Purchase History" entry in your App Privacy page. +## Privacy -You can answer all subsequent questions with "No" because we neither link collected data to the users identity, nor do we use them for tracking purposes. +If you already use RevenueCat or a similar service, you're already sending more data to them than TelemetryDeck collects. No privacy label changes needed. -When all is configured your "Purchases" entry in your App Privacy page should end up looking like this: +If tracking purchases directly, mark "Analytics" under "Purchase History" in your App Privacy page. Answer all subsequent questions with "No". ![Purchases entry with only 'Used for Analytics' in the box](/assets/purchases-privacy-box.png) diff --git a/docs/articles/swift-custom-processors.md b/docs/articles/swift-custom-processors.md new file mode 100644 index 0000000..98e12cd --- /dev/null +++ b/docs/articles/swift-custom-processors.md @@ -0,0 +1,242 @@ +--- +title: Writing Custom Processors +tags: + - Swift + - SDK + - processors + - advanced +testedOn: SwiftSDK 3.0.0 +description: Build your own EventProcessor to enrich, filter, or transform events in the TelemetryDeck Swift SDK pipeline. +lead: Add domain-specific metadata to every event by writing your own processor and plugging it into the pipeline. +--- + +## The EventProcessor protocol + +A processor is any `Sendable` type that conforms to `EventProcessor`: + +```swift +public protocol EventProcessor: Sendable { + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event + + func start( + storage: any ProcessorStorage, + logger: any Logging, + emitter: any EventSending + ) async + + func stop() async +} +``` + +Only `process` is required. The `start` and `stop` methods have default no-op implementations. + +## A minimal processor + +Here's a processor that adds the current A/B test variant to every event: + +```swift +struct ABTestProcessor: EventProcessor { + let variant: String + + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event { + var ctx = context + ctx.addParameter("ABTest.variant", value: variant) + return try await next(input, ctx) + } +} +``` + +Key points: + +- Mutate `context` to add metadata parameters. These are merged with the event at finalization. +- Mutate `input` to change the event name, parameters, or float value. +- Always call `next(input, context)` to pass the event down the pipeline — unless you want to drop it. +- Input parameters override context parameters when keys collide. + +## Registering your processor + +Pass your processors when initializing TelemetryDeck. Use `defaultProcessors()` to keep the built-in ones: + +```swift +try await TelemetryDeck.initialize( + configuration: TelemetryDeck.Config( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ), + processors: TelemetryDeck.defaultProcessors() + [ + ABTestProcessor(variant: "B") + ] +) +``` + +Your processor runs after all default processors. To insert it at a specific position, build the array manually: + +```swift +var processors = TelemetryDeck.defaultProcessors() +processors.insert(ABTestProcessor(variant: "B"), at: 0) + +try await TelemetryDeck.initialize( + configuration: config, + processors: processors +) +``` + +## Filtering events + +Drop events by throwing `ProcessorError.eventFiltered`: + +```swift +struct InternalScreenFilter: EventProcessor { + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event { + if input.name.hasPrefix("Internal.") { + throw ProcessorError.eventFiltered + } + return try await next(input, context) + } +} +``` + +Filtered events are silently discarded. + +## Transforming events + +Modify the event name or parameters before passing them on: + +```swift +struct ScreenNameSanitizer: EventProcessor { + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event { + var modified = input + modified.name = modified.name + .replacingOccurrences(of: " ", with: ".") + return try await next(modified, context) + } +} +``` + +## Using persistent storage + +Processors that need to persist state across app launches get access to `ProcessorStorage` in their `start` method: + +```swift +actor SubscriptionTierProcessor: EventProcessor { + private var storage: (any ProcessorStorage)? + private var tier: String = "free" + + func start( + storage: any ProcessorStorage, + logger: any Logging, + emitter: any EventSending + ) async { + self.storage = storage + if let saved = await storage.string(forKey: "subscriptionTier") { + tier = saved + } + } + + func updateTier(_ newTier: String) async { + tier = newTier + await storage?.set(newTier, forKey: "subscriptionTier") + } + + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event { + var ctx = context + ctx.addParameter("Subscription.tier", value: tier) + return try await next(input, ctx) + } +} +``` + +`ProcessorStorage` supports `Data`, `String`, `Int`, and `Bool` types. The default implementation backs onto `UserDefaults` with a suite scoped to your app ID. + +## Emitting events from a processor + +The `emitter` parameter in `start` lets a processor send its own events back into the pipeline — for example, system-level events the processor generates independently: + +```swift +actor WatchdogProcessor: EventProcessor { + private var emitter: (any EventSending)? + + func start( + storage: any ProcessorStorage, + logger: any Logging, + emitter: any EventSending + ) async { + self.emitter = emitter + } + + func reportAnomaly(_ description: String) async { + await emitter?.send(EventInput( + name: "Watchdog.anomalyDetected", + parameters: ["description": description] + )) + } + + func process( + _ input: EventInput, + context: EventContext, + next: @Sendable (EventInput, EventContext) async throws -> Event + ) async throws -> Event { + return try await next(input, context) + } +} +``` + +Events sent via `emitter` go through the entire pipeline, including your processor. + +## Replacing built-in processors + +If a built-in processor doesn't fit your needs, leave it out of the pipeline and supply your own. For example, to replace the session tracking logic: + +```swift +try await TelemetryDeck.initialize( + configuration: config, + processors: TelemetryDeck.defaultProcessors() + .filter { $0 is SessionTrackingProcessor == false } + + [MyCustomSessionProcessor()] +) +``` + +!!! warning + + If you remove built-in processors, features that depend on them (like retention metrics or test mode detection) will stop working. Only do this when you have a specific reason. + +## Testing with SpyEventTransmitter + +The SDK provides `SpyEventTransmitter` and `InMemoryEventCache` for testing. Inject them to capture events without hitting the network: + +```swift +let spy = SpyEventTransmitter() + +try await TelemetryDeck.initialize( + configuration: config, + processors: [MyProcessor()], + cache: InMemoryEventCache(), + transmitter: spy +) + +await TelemetryDeck.event("test.event") +await TelemetryDeck.flush() + +let transmitted = await spy.transmittedEvents +assert(transmitted.contains { $0.type == "test.event" }) +``` diff --git a/docs/articles/swift-processors.md b/docs/articles/swift-processors.md new file mode 100644 index 0000000..33006d1 --- /dev/null +++ b/docs/articles/swift-processors.md @@ -0,0 +1,188 @@ +--- +title: How Events Flow Through Processors +tags: + - Swift + - SDK + - processors +testedOn: SwiftSDK 3.0.0 +description: Understand how the TelemetryDeck Swift SDK processes events through its middleware pipeline before transmission. +lead: Every event passes through a chain of processors that enrich it with device info, session data, and retention metrics before it reaches the server. +--- + +## The processor pipeline + +When you call `TelemetryDeck.event(...)`, the event passes through a pipeline of **processors** before transmission. Each processor can inspect, enrich, transform, or filter the event. + +```mermaid +graph TD + A["TelemetryDeck.event(...)"] --> B[PreviewFilter] + B --> C[DefaultParameters] + C --> D[DefaultPrefix] + D --> E[Validation] + E --> F[TestMode] + F --> G[UserIdentifier] + G --> H[SessionTracking] + H --> I[Device] + I --> J[AppInfo] + J --> K[Locale] + K --> L[Calendar] + L --> M[Accessibility] + M --> N[TrialConversion] + N --> O[Finalizer] + O --> P[Event Cache] + P --> Q[HTTP Transmitter] +``` + +Each processor receives two things: + +- **EventInput** — the event name, parameters, and optional float value +- **EventContext** — mutable metadata that accumulates as the event moves through the pipeline (session ID, user identifier, test mode flag, and additional parameters) + +A processor can modify either of these, then call `next` to pass control to the next processor. It can also throw `ProcessorError.eventFiltered` to silently drop the event. + +At the end of the pipeline, the **finalizer** merges everything into a single `Event` object, hashes the user identifier with SHA256, and hands it to the cache for transmission. + +## Built-in processors + +The SDK ships with a default set of processors. They run in this order: + +### PreviewFilterProcessor + +Detects Xcode SwiftUI previews and drops all events during preview rendering. You never see preview noise in your dashboard. + +### DefaultParametersProcessor + +Injects the `defaultParameters` you provided at initialization. These appear on every event. Your per-event parameters override them if keys collide. + +### DefaultPrefixProcessor + +Prepends the `eventPrefix` to event names and `parameterPrefix` to custom parameter keys. SDK-internal parameters (prefixed with `TelemetryDeck.`) are not affected. + +For example, with `eventPrefix: "MyApp."`, calling `event("login")` produces `MyApp.login`. + +### ValidationProcessor + +Warns in the console if your event names or parameter keys collide with reserved `TelemetryDeck.*` names. Events pass through unchanged. + +### TestModeProcessor + +Determines whether the event should be marked as test data. In `DEBUG` builds, test mode is on by default. You can override this with the `testMode` parameter during initialization. + +### UserIdentifierProcessor + +Attaches a user identifier to the event context. The SDK auto-resolves a default identifier per platform (IDFV on iOS, a persisted UUID on macOS). You can override it with `TelemetryDeck.setUserIdentifier(_:)` or pass a `customUserID` per event. See [User Identification](/articles/swift-user-identification/) for full details on resolution order, platform behavior, and hashing. + +### SessionTrackingProcessor + +Manages session lifecycle and retention metrics. It: + +- Generates a session ID at startup +- Starts a new session when the app returns from background after 5+ minutes +- Fires `TelemetryDeck.Session.started` on each new session (configurable) +- Detects first-ever launch and fires `TelemetryDeck.Acquisition.newInstallDetected` +- Adds retention parameters to every event: + +| Parameter | Type | Description | +|---|---|---| +| `TelemetryDeck.Retention.totalSessionsCount` | Int | Total sessions across all app launches | +| `TelemetryDeck.Retention.distinctDaysUsed` | Int | Unique calendar days the app was used | +| `TelemetryDeck.Retention.distinctDaysUsedLastMonth` | Int | Unique days used in the last 30 days | +| `TelemetryDeck.Retention.averageSessionSeconds` | Int | Average session length (-1 if only one session) | +| `TelemetryDeck.Retention.previousSessionSeconds` | Int | Length of the previous session (if ≥2 sessions) | +| `TelemetryDeck.Acquisition.firstSessionDate` | String | ISO 8601 date of first session | +| `TelemetryDeck.Acquisition.isNewInstall` | Bool | `true` only on the first event of the first session | + +Session history is persisted for 90 days. Older sessions are counted but their details are pruned. + +### DeviceProcessor + +Adds hardware and platform information: + +| Parameter | Example | +|---|---| +| `TelemetryDeck.Device.platform` | `iOS` | +| `TelemetryDeck.Device.operatingSystem` | `iOS` | +| `TelemetryDeck.Device.systemVersion` | `iOS 18.0.0` | +| `TelemetryDeck.Device.systemMajorVersion` | `iOS 18` | +| `TelemetryDeck.Device.systemMajorMinorVersion` | `iOS 18.0` | +| `TelemetryDeck.Device.modelName` | `iPhone16,2` | +| `TelemetryDeck.Device.architecture` | `iPhone16,2` (iOS) / `arm64` (macOS) | +| `TelemetryDeck.Device.timeZone` | `UTC+2` | +| `TelemetryDeck.RunContext.isSimulator` | `false` | +| `TelemetryDeck.RunContext.isDebug` | `true` | +| `TelemetryDeck.RunContext.isTestFlight` | `false` | +| `TelemetryDeck.RunContext.isAppStore` | `false` | +| `TelemetryDeck.RunContext.targetEnvironment` | `native` | + +### AppInfoProcessor + +Adds app and SDK version information: + +| Parameter | Example | +|---|---| +| `TelemetryDeck.AppInfo.version` | `2.1.0` | +| `TelemetryDeck.AppInfo.buildNumber` | `42` | +| `TelemetryDeck.AppInfo.versionAndBuildNumber` | `2.1.0 (build 42)` | +| `TelemetryDeck.SDK.name` | `SwiftSDK` | +| `TelemetryDeck.SDK.version` | `3.0.0` | +| `TelemetryDeck.SDK.nameAndVersion` | `SwiftSDK 3.0.0` | + +### LocaleProcessor + +Adds language and region preferences: + +| Parameter | Example | +|---|---| +| `TelemetryDeck.RunContext.locale` | `en_US` | +| `TelemetryDeck.RunContext.language` | `en` | +| `TelemetryDeck.UserPreference.language` | `de` | +| `TelemetryDeck.UserPreference.region` | `US` | + +### CalendarProcessor + +Adds temporal context for time-based analysis: + +| Parameter | Example | +|---|---| +| `TelemetryDeck.Calendar.dayOfMonth` | `15` | +| `TelemetryDeck.Calendar.dayOfWeek` | `3` | +| `TelemetryDeck.Calendar.dayOfYear` | `166` | +| `TelemetryDeck.Calendar.weekOfYear` | `24` | +| `TelemetryDeck.Calendar.isWeekend` | `false` | +| `TelemetryDeck.Calendar.monthOfYear` | `6` | +| `TelemetryDeck.Calendar.quarterOfYear` | `2` | +| `TelemetryDeck.Calendar.hourOfDay` | `14` | + +### AccessibilityProcessor + +Captures accessibility settings and display properties. Results are cached for 1 hour to avoid excessive main-thread access. + +| Parameter | Example | +|---|---| +| `TelemetryDeck.Accessibility.isBoldTextEnabled` | `false` | +| `TelemetryDeck.Accessibility.isReduceMotionEnabled` | `false` | +| `TelemetryDeck.Accessibility.preferredContentSizeCategory` | `L` | +| `TelemetryDeck.UserPreference.colorScheme` | `Dark` | +| `TelemetryDeck.UserPreference.layoutDirection` | `leftToRight` | +| `TelemetryDeck.Device.screenResolutionWidth` | `430.0` | +| `TelemetryDeck.Device.screenResolutionHeight` | `932.0` | + +### TrialConversionProcessor + +Available on iOS 15, macCatalyst 15, macOS 12, tvOS 15, and watchOS 8. Monitors StoreKit `Transaction.updates` in the background and automatically fires `TelemetryDeck.Purchase.convertedFromTrial` when a user transitions from a free trial to a paid subscription. No setup required — it runs silently. + +## Event caching and transmission + +After processing, events land in a persistent cache (up to 10,000 events). The transmitter sends batched events every 10 seconds to `{apiBaseURL}/v2/namespace/{namespace}/`. + +If transmission fails, the SDK retries with exponential backoff (doubling the interval, capped at 5 minutes). Events that receive permanent HTTP errors (400, 401, 403, 404, 413, 422, 501, 505) are dropped immediately. + +You can force an immediate flush: + +```swift +await TelemetryDeck.flush() +``` + +## Pre-initialization buffering + +Events sent before `initialize()` completes are buffered in memory and replayed once initialization finishes. Event ordering is preserved. diff --git a/docs/articles/swift-user-identification.md b/docs/articles/swift-user-identification.md new file mode 100644 index 0000000..068ba6c --- /dev/null +++ b/docs/articles/swift-user-identification.md @@ -0,0 +1,87 @@ +--- +title: User Identification +tags: + - Swift + - SDK + - advanced +testedOn: SwiftSDK 3.0.0 +description: How the TelemetryDeck Swift SDK resolves, hashes, and transmits user identifiers across Apple platforms and Linux. +lead: The SDK assigns each user a stable, anonymous identifier. Here's how it resolves that identifier on each platform, how you can override it, and how it's hashed before transmission. +--- + +## Resolution order + +When an event is processed, the SDK picks the user identifier from the first available source: + +1. **Per-event override** — the `customUserID` parameter on `TelemetryDeck.event(...)` +2. **Explicit identifier** — set at runtime via `TelemetryDeck.setUserIdentifier(_:)` +3. **Default identifier** — from the `defaultUser` initialization parameter, or auto-resolved from the platform + +If you never call `setUserIdentifier` and don't pass `defaultUser` at init, the SDK resolves a default automatically. + +## Platform defaults + +| Platform | Source | +|---|---| +| iOS, tvOS, visionOS | [`UIDevice.identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor) | +| watchOS | `WKInterfaceDevice.identifierForVendor` | +| macOS | Random UUID, generated once and persisted in `UserDefaults` | +| Linux / server | Static string derived from platform and app version | + +### identifierForVendor (iOS, tvOS, visionOS, watchOS) + +On Apple's mobile and wearable platforms, the SDK uses `identifierForVendor` (IDFV). This is a UUID assigned by the OS that: + +- Stays stable across launches and app updates +- Is shared across all apps from the same vendor on a given device +- Resets when the user uninstalls **all** of that vendor's apps from the device +- Differs between TestFlight and the App Store, even for the same build — a tester's IDFV in TestFlight will not match their IDFV after installing from the App Store + +See Apple's [identifierForVendor documentation](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor) for the full lifecycle rules, including edge cases around app groups and enterprise distribution. + +If `identifierForVendor` returns `nil` — which can happen before the device is first unlocked after a restart — the SDK falls back to a generic string based on platform and app version. Different users on the same app version temporarily share this identifier until IDFV becomes available. The identifier is resolved once at startup with no retry. + +### macOS + +macOS has no `identifierForVendor`. On first launch, the SDK generates a random UUID and stores it in a `UserDefaults` suite scoped to your app ID. Subsequent launches read the same UUID. Deleting the app's UserDefaults data resets the identifier. + +### Linux and server-side Swift + +The SDK returns a static string derived from the platform name and app version. Every user on the same deployment gets the same identifier. For server-side use, pass an explicit user identifier: + +```swift +await TelemetryDeck.event("API.requestHandled", customUserID: request.authenticatedUserID) +``` + +## Setting a user identifier + +Override the default at any point after initialization: + +```swift +await TelemetryDeck.setUserIdentifier("user@example.com") +``` + +Pass `nil` to revert to the platform default: + +```swift +await TelemetryDeck.setUserIdentifier(nil) +``` + +For a single event: + +```swift +await TelemetryDeck.event("checkout.completed", customUserID: order.userID) +``` + +## Hashing + +The raw identifier is SHA256-hashed at the end of the processor pipeline, just before the event enters the cache. The hash input is `identifier + salt`, where `salt` is the value you pass to `initialize(appID:namespace:salt:)` (empty string by default). + +The result is a 64-character lowercase hex string. The unhashed identifier never leaves the device. The TelemetryDeck server applies an additional server-side salt, so even if two apps use the same user email with the same client salt, the final stored hashes differ between apps. + +## Recommendations + +- **Most apps**: the platform default (IDFV) is sufficient. Users are tracked per-device, anonymously. +- **Cross-device identity**: if you want to correlate the same user across iPhone and iPad, call `setUserIdentifier` with a stable account identifier (email, user ID). It gets hashed before transmission. +- **Server-side Swift**: always pass `customUserID` — the default identifier is meaningless on servers. +- **Salt**: adding a `salt` at initialization provides an extra layer — even if someone intercepts the hashed identifier, they can't reverse it against a known-plaintext attack without knowing your salt. diff --git a/docs/articles/telemetry-client.md b/docs/articles/telemetry-client.md index e46aaea..bd69079 100644 --- a/docs/articles/telemetry-client.md +++ b/docs/articles/telemetry-client.md @@ -1,107 +1,30 @@ --- -title: TelemetryDeck Swift Client Reference +title: TelemetryClient (Legacy) tags: - Swift - SDK -testedOn: Xcode 12.4 & Swift 5.3 -description: Reference documentation for the Swift Client for TelemetryDeck-using apps -lead: The TelemetryDeck Swift Client is a Swift Package to include in your app -searchEngineTitle: How to add the TelemetryDeck Swift Client -searchEngineDescription: Learn how to add the Swift Client for TelemetryDeck-using apps + - legacy +description: Legacy reference for the TelemetryClient/TelemetryManager API. Replaced by the TelemetryDeck API in SwiftSDK 2.x and removed in 3.0. +lead: This page documents the original TelemetryClient API, which was replaced by the TelemetryDeck static API. If you're still using these APIs, migrate to the current SDK. --- -## Include the Swift Client in your Xcode Project +!!! warning "Outdated" -See the [Swift Guide](/docs/guides/swift-setup/) on how to set up your app to use the TelemetryDeck Swift Client. + The APIs on this page (`TelemetryManager`, `TelemetryClient`, `TelemetryManagerConfiguration`) were removed in SwiftSDK 3.0. See the [Swift Setup Guide](/guides/swift-setup/) for current documentation or the [V3 Migration Guide](/guides/swift-migration-v3/) if upgrading. -## Initialization +## Historical context -Init the TelemetryDeck at app startup, so it knows your App ID (you can retrieve the App ID in the TelemetryDeck Viewer app, under App Settings) +The original Swift SDK (called "SwiftClient") used a `TelemetryManager` singleton with a `TelemetryManagerConfiguration` class. These were replaced by the `TelemetryDeck` static API in the [Grand Rename](/articles/grand-rename/) and formally removed in SwiftSDK 3.0. -```swift -let config = TelemetryDeck.Config(appID: "") -TelemetryDeck.initialize(config: config) -``` +## Quick migration reference -For example, if you're building a scene based app, in the `init()` function for your `App`: - -```swift -import SwiftUI -import TelemetryDeck - -@main -struct TelemetryTestApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } - - init() { - // Note: Do not add this code to `WindowGroup.onAppear`, which will be called - // *after* your window has been initialized, and might lead to out initialization - // occurring too late. - let config = TelemetryDeck.Config(appID: "") - TelemetryDeck.initialize(config: config) - } -} -``` - -## Sending Signals - -Once you've included the TelemetryDeck Swift Client, send signals like so: - -```swift -TelemetryDeck.signal"appLaunchedRegularly") -``` - -TelemetryDeck will create a user identifier for your user that is specific to app installation and device. If you have a better user identifier available, you can use that instead: (the identifier will be hashed before sending it) - -```swift -let email = MyConfiguration.User.Email -TelemetryDeck.signal"userLoggedIn", customUserID: email) -``` - -## Payload Data - -You can also send additional parameters with each signal: - -```swift -TelemetryDeck.signal("databaseUpdated", parameters: ["numberOfDatabaseEntries": "3831"]) -``` - -TelemetryDeck will automatically send base parameters with these keys: - -- `TelemetryDeck.AppInfo.buildNumber` -- `TelemetryDeck.AppInfo.dartVersion` -- `TelemetryDeck.AppInfo.version` -- `TelemetryDeck.AppInfo.versionAndBuildNumber` -- `TelemetryDeck.Device.architecture` -- `TelemetryDeck.Device.brand` -- `TelemetryDeck.Device.modelName` -- `TelemetryDeck.Device.operatingSystem` -- `TelemetryDeck.Device.orientation` -- `TelemetryDeck.Device.platform` -- `TelemetryDeck.Device.screenResolutionWidth` -- `TelemetryDeck.Device.screenResolutionHeight` -- `TelemetryDeck.Device.systemMajorVersion` -- `TelemetryDeck.Device.systemMajorMinorVersion` -- `TelemetryDeck.Device.systemVersion` -- `TelemetryDeck.Device.timeZone` -- `TelemetryDeck.RunContext.extensionIdentifier` -- `TelemetryDeck.RunContext.isAppStore` -- `TelemetryDeck.RunContext.isDebug` -- `TelemetryDeck.RunContext.isSimulator` -- `TelemetryDeck.RunContext.isTestFlight` -- `TelemetryDeck.RunContext.language` -- `TelemetryDeck.RunContext.locale` -- `TelemetryDeck.RunContext.targetEnvironment` -- `TelemetryDeck.SDK.name` -- `TelemetryDeck.SDK.nameAndVersion` -- `TelemetryDeck.SDK.version` -- `TelemetryDeck.UserPreference.region` -- `TelemetryDeck.UserPreference.language` - -## Debug Mode - -TelemetryDeck will _not_ send any signals if you are in `DEBUG` Mode. You can override this by setting `configuration.telemetryAllowDebugBuilds = true` on your `TelemetryDeck.Configuration` instance. +| Old API | Current API | +|---|---| +| `TelemetryManagerConfiguration(appID:)` | `TelemetryDeck.initialize(appID:namespace:)` | +| `TelemetryManager.initialize(config:)` | `TelemetryDeck.initialize(appID:namespace:)` | +| `TelemetryManager.send("event")` | `await TelemetryDeck.event("event")` | +| `TelemetryManager.send("event", for: user)` | `await TelemetryDeck.event("event", customUserID: user)` | +| `TelemetryManager.send("event", with: params)` | `await TelemetryDeck.event("event", parameters: params)` | +| `TelemetryManager.shared.hashedDefaultUser` | Not exposed directly; use `setUserIdentifier(_:)` | +| `configuration.telemetryAllowDebugBuilds = true` | `testMode: false` parameter in `initialize` | +| `import TelemetryClient` | `import TelemetryDeck` | diff --git a/docs/articles/test-mode.md b/docs/articles/test-mode.md index 7c67e95..f3b1d38 100644 --- a/docs/articles/test-mode.md +++ b/docs/articles/test-mode.md @@ -1,75 +1,67 @@ --- -title: Getting started with Test Mode +title: Test Mode tags: - setup - testmode - quickstart - beginner -testedOn: Xcode 13.1 & Swift 5.5 & TelemetryDeck SDK 1.1.5 -description: Here's how to use Test Mode to get started with TelemetryDeck -lead: Test Mode helps you make sure that TelemetryDeck is set up correctly in your app and allows you to set up your insights even during development. +testedOn: SwiftSDK 3.0.0 +description: Use Test Mode to verify your TelemetryDeck setup without polluting production data. +lead: Test Mode keeps your development and testing signals separate from real user data. searchEngineTitle: How to run test signals -searchEngineDescription: With test mode, you can review your analytics setup without compromising live data. Enable Test Mode in your dashboard to send signals through debug mode. +searchEngineDescription: With test mode, you can review your analytics setup without compromising live data. --- -During the development of your TelemetryDeck-enabled app - or even while you test it - your app sends signals. These signals are not from your users but rather from yourself or your development team. You might even send hundreds of signals during tests, which would mess up your insights if mixed with actual analytics data. Not cool! +During development and testing, your app sends events that aren't from real users. Test Mode keeps these separate from production analytics. -We do not recommend not doing any testing. The benefits of sending signals during test phases are enormous! If you have not considered it yet, here are some nifty reasons why you should start now: +Benefits of sending test signals: -- You will be able to find errors in the configuration of the TelemetryDeck SDK -- Working with test signals means you will know if your app works even before releasing your app -- As well as being able to make preparations for new signal types or payload types until your app is released - -Test Mode will let you easily and quickly test new features for your app, giving you the power to release the best product possible! Let's dive right in. +- Verify your TelemetryDeck SDK configuration before release +- Confirm new event types and parameters work as expected +- Set up insights and dashboards ahead of launch ## How it works -Each sent signal has a `isTestMode` parameter, which can either be `true` or `false`. -Navigate to your TelemetryDeck [dashboard](https://dashboard.telemetrydeck.com/), where you will find the Test Mode toggle on the top left side, just above the sidebar. -You can toggle it to show your signals either in `isTestMode == true` or `isTestMode == false`. While toggled to `true`, you will see a banner at the top displaying **Test Data** to remind you that you are currently in Test Mode, and all signals get sent in said mode. All charts will display test data only while in Test Mode. +Every event has an `isTestMode` flag. The TelemetryDeck Dashboard has a Test Mode toggle in the upper left — flip it to switch between test and production data. A banner reminds you when you're viewing test data. ![Screenshot of the dashboard showing the Test Mode toggle in the upper left corner.](/assets/test_mode.png) -## Sending Signals in Test Mode +## Automatic detection + +The SDKs detect test mode automatically. In the Swift SDK, events sent from `DEBUG` builds are marked as test signals by default. + +## Manual override -The SDKs try to infer the isTestMode parameter as best as they can. For example, if a DEBUG parameter is present in your development environment, that is used as the value for isTestMode. -You can also override the isTestMode parameter just as you would add any other payload parameter to a signal -Note: since signal payloads only support strings, the parameter needs to be either "true" or "false" +### Swift SDK -### Manually set Test Mode in Swift SDK +Override test mode at initialization: ```swift -// An example variable to manually set test mode. -// Set this to `true` or `false` depending on your app's configuration -// or environment or state -let customTestModeParameter = true - -TelemetryManager.send( - "pizzaModeActivated", - for: "myUserIdentifier", - with: ["isTestMode": customTestModeParameter ? "true" : "false"] -)` +try await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE", + testMode: true +) +``` + +Check the current test mode state at runtime: + +```swift +let isTest = await TelemetryDeck.isTestMode() +``` + +### JavaScript SDK + +```javascript +const td = new TelemetryDeck({ + app: "YOUR-APP-ID", + user: "anonymous", + testMode: true +}); ``` -### Manually set Test Mode in JavaScript SDK +Or set it per-signal: ```javascript -// Example initialisation of TelemetryDeck SDK -`td = new TelemetryDeck({ - app: ENV.APP.telemetryAppID, - user: this.user.current?.email ?? 'anonymous', -});` - -// In our example, the app has a `send` function wrapping the TelemetryDeck SDK -send(payload) { - // ENV.APP.telemetryIsDebug is an example variable that represents your app's - // configuration or environment. Replace it with an implementation that fits your - // app's needs. - if (ENV.APP.telemetryIsDebug) { - this.td.signal({...payload, isTestMode: "true"}) - return; - } - - this.td.signal(payload); -} +td.signal("eventName", { isTestMode: "true" }); ``` diff --git a/docs/guides/objective-c-setup.md b/docs/guides/objective-c-setup.md index 5258698..a8fb893 100644 --- a/docs/guides/objective-c-setup.md +++ b/docs/guides/objective-c-setup.md @@ -4,153 +4,89 @@ tags: - Setup - iOS - macOS - - watchOS - - tvOS - ObjectiveC featured: false -testedOn: Xcode 14.1 & Swift 5.5 +testedOn: Xcode 26 & SwiftSDK 3.0.0 description: Configure the TelemetryDeck SDK in Your Objective-C Application for iOS and macOS -lead: Let's include the TelemetryClient Swift Package in your Objective-C application and send events! +lead: Include the TelemetryDeck Swift Package in your Objective-C application and send events. order: 1000 --- -Lots of iOS and macOS application are still written in Objective-C, or a mixture of Swift and ObjC. We've got you covered either way, and this guide will show you how to set up the TelemetryDeck SDK in your Objective-C application. +Objective-C apps (or mixed Swift/ObjC projects) can use the TelemetryDeck Swift SDK through Objective-C interop. ## Installing the package -The TelemetryDeck Swift package uses Swift Package Manager. +1. Open Xcode and navigate to your project +1. Select FileAdd Package Dependencies... +1. Paste `https://github.com/TelemetryDeck/SwiftSDK` into the search field +1. Select the `SwiftSDK` package +1. Set the Dependency Rule to Up to Next Major Version +1. Click Add Package -1. Open Xcode and navigate to the project you want to add TelemetryDeck to. -1. In the menu, select File -> Add Packages.... This will open the Swift Package Manager view. -1. Paste `https://github.com/TelemetryDeck/SwiftSDK` into the search field. -1. Select the `SwiftSDK` package that appears in the list -1. Set the Dependency Rule to Up to Next Major Version. -1. Click Add Package. +## Linking the package -![A screenshot of Xcode adding the TelemetryDeck Package](/assets/xcode-swift-package.png) +In the Choose Package Products for SwiftSDK screen, select the `TelemetryDeck` library and click Add Package. -This will include the TelemetryDeck Swift Client into your app by downloading the source code. Feel free to browse the client's source code. It's tiny and you'll see for yourself how TelemetryDeck is hashing user identifiers before they ever reach the server. Privacy, yay! +!!! note "Multiple targets" -## Including the package in your target + If Xcode doesn't prompt you, add it manually: select your target → Build PhasesLink Binary With Libraries+ → select `TelemetryDeck`. -Xcode will ask you to link the package with your target in the next screen, titles Choose Package Products for SwiftSDK. Select the `TelemetryClient` library and click Add Package. +## Initializing TelemetryDeck -{% noteinfo "Link Library with more than one Target" %} - -In case Xcode forgets to ask you to link the library with your target, you can do so manually by selecting your target in the project navigator. Selecting the Build Phases tab. Click the + button in the Link Binary With Libraries section and select the `TelemetryClient` library. -{% endnoteinfo %} - -## Initializing the TelemetryDeck package - -The `TelemetryClient` package will provide you with a class `TelemetryManager` that you'll use for all interactions with TelemetryDeck. Before you can use that class, you'll need to initialize it at the start of your app. The ideal place for this is your **App Delegate**'s `application:didFinishLaunchingWithOptions:` method: +Add initialization to your App Delegate's `application:didFinishLaunchingWithOptions:`: ```objc -// Import the TelemetryClient library -@import TelemetryClient; - -// ... - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { +@import TelemetryDeck; - // Your other initialization code here ... +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Initialize the TelemetryDeck client - TelemetryManagerConfiguration *telemetryConfig = - [[TelemetryManagerConfiguration alloc] initWithAppID:@"YOUR-APP-ID"]; - [TelemetryManager initializeWith:telemetryConfig]; - - // Optional: Set a default user ID. If you don't do this, - // the SDK will generate a random user ID for you. - // [TelemetryManager updateDefaultUserTo:@"myuserwhojustloggedin@example.com"]; + [TelemetryDeck initializeWithAppID:@"YOUR-APP-ID" + namespace:@"YOUR-NAMESPACE"]; return YES; } ``` -{% noteinfo "You need your app's Unique Identifier" %} -TelemetryDeck assigns a unique identifier to your app, and you need to hand that identifier to the TelemetryDeck SDK. +!!! note "You need your app's unique identifier and namespace" -Use the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) to create a new app and copy its unique identifier into your computer's clipboard. -{% endnoteinfo %} + Both values are available in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) under your app's settings. ## Verify your setup -Run your app to verify that TelemetryDeck is properly integrated. Let's send an event to show the app has launched correctly: +Run your app. The SDK automatically sends a `TelemetryDeck.Session.started` event on launch. -```objc -[TelemetryManager send:@"applicationDidFinishLaunching"]; -``` +!!! warning "Test signals" -{% notewarning "When running from Xcode, you're sending test signals" %} -If your app is built in `DEBUG` configuration (i.e. running from Xcode), your events will be tagged as **Test Signals**, meaning that you can easily filter them out later. You'll see them show up in the TelemetryDeck Dashboard when the **Test Mode** toggle under the tab bar is turned on. -{% endnotewarning %} + Events from `DEBUG` builds are tagged as test signals. Enable **Test Mode** in the TelemetryDeck Dashboard to see them. -Open the TelemetryDeck Dashboard, navigate to "Explore > Recent Signals" and make sure "Test Mode" is enabled. You should see your signal appear after launching your app. +Open the Dashboard → "Explore > Recent Signals" with Test Mode enabled. --- -{% noteinfo "Ready for basic insights" %} -Congratulations! With just the SDK initialization, TelemetryDeck will automatically track user sessions, app launches, and device information. This basic setup provides valuable built-in insights without any additional code. - -You can now build and ship your app. Once users start using it, your TelemetryDeck dashboard will begin showing data about user behavior, device types, and other key metrics. -{% endnoteinfo %} - -## Enhancing your analytics (optional) +## Sending custom events -While basic session tracking provides valuable information, sending custom events lets you answer questions specific to how users engage with *your* app. - -### Sending custom events +```objc +[TelemetryDeck event:@"pizzaModeActivated"]; +``` -{% noteinfo "What's a signal?" %} -Signals represent an **event** or a **view** that happened in your app, which is used by a **user**. Signals consist of these parts: +With parameters: -- **Signal Type**: A string that indicates which kind of event happened -- **Metadata Payload**: A dictionary of additional data about your app or the event triggering the signal +```objc +[TelemetryDeck event:@"pizzaModeActivated" + parameters:@{@"cheeseMode": @"extraCheesy"}]; +``` -See the [Signals Reference](/docs/api/signals-reference/) for more information about how you can effectively use Signals. -{% endnoteinfo %} +## App Store requirements -You don't need to keep an instance of TelemetryManager and hand it around, just call the `send` function on the class directly. If you want to add custom metadata payload, add it to the function call as a dictionary: +See our [Apple App Privacy guide](/articles/apple-app-privacy/) and [Privacy FAQ](/guides/privacy-faq/#do-i-need-to-add-telemetrydeck-to-my-privacy-policy%3F). -```objc -[ - TelemetryManager - send:@"applicationDidFinishLaunching" - with:@{@"pizzaCheeseMode": @"extraCheesy"} -]; -``` +## What's next -The metadata is helpful for additional parameters for filtering or grouping signals. We'll automatically add some metadata for you, like the app version, device model, and more. +
-For more information on how to send events, see the [TelemetryDeck package's `README.md` file](https://github.com/TelemetryDeck/SwiftSDK/blob/main/README.md). +- **[Analytics Walkthrough](/basics/index)** -## App Store requirements + Navigate TelemetryDeck, interpret insights, and make data-driven decisions. -Before uploading your app to the App Store, you'll need to complete Apple's privacy details on App Store Connect. Although TelemetryDeck is privacy-focused, you still need to disclose analytics usage. - -For guidance on completing these requirements, see our [Apple App Privacy guide](/docs/articles/apple-app-privacy/). - -For privacy policy recommendations, check our [Privacy FAQ](/docs/guides/privacy-faq/#do-i-need-to-add-telemetrydeck-to-my-privacy-policy%3F). - -## What to do next - -Now that you've integrated TelemetryDeck, learn how to use the analytics platform to gain valuable insights about your users: - -
-
-
- -
-

- - 📊 Analytics Walkthrough -

-

Learn how to navigate TelemetryDeck, interpret insights, and use analytics to make data-driven decisions that improve your app and grow your user base.

-

- Start here to get real value from your analytics - -

-
-
-
diff --git a/docs/guides/swift-migration-v3.md b/docs/guides/swift-migration-v3.md new file mode 100644 index 0000000..1c90627 --- /dev/null +++ b/docs/guides/swift-migration-v3.md @@ -0,0 +1,210 @@ +--- +title: Migrating from SwiftSDK 2.x to 3.0 +tags: + - Swift + - SDK + - migration +testedOn: SwiftSDK 3.0.0 +description: What changed in TelemetryDeck SwiftSDK 3.0 and how to update your code. +lead: SwiftSDK 3.0 replaces providers with processors, requires a namespace, and makes the API async. Here's how to migrate. +order: 200 +--- + +## Platform requirements + +SwiftSDK 3.0 raises minimum deployment targets: + +| Platform | V2 | V3 | +|---|---|---| +| iOS | 12 | 15 | +| macOS | 10.13 | 12 | +| watchOS | 5 | 8 | +| tvOS | 13 | 15 | +| visionOS | 1 | 1 | + +Swift 6.2 and Xcode 26 are required. + +## Breaking changes + +### Namespace is required + +`TelemetryDeck.Config` now requires a `namespace` parameter. You'll find your namespace in the TelemetryDeck Dashboard alongside your app ID. + +=== "Before (V2)" + + ```swift + let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") + TelemetryDeck.initialize(config: config) + ``` + +=== "After (V3)" + + ```swift + try await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + ``` + +### Initialization is async + +`initialize` is now `async throws`. Wrap it in a `Task` when calling from synchronous contexts like `init()`: + +```swift +init() { + Task { + try? await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + } +} +``` + +Events sent before initialization completes are buffered and delivered automatically. + +### signal() → event() + +The `signal` function is deprecated. Replace it with `event`: + +=== "Before (V2)" + + ```swift + TelemetryDeck.signal("buttonTapped") + TelemetryDeck.signal("purchase", parameters: ["item": "widget"]) + ``` + +=== "After (V3)" + + ```swift + await TelemetryDeck.event("buttonTapped") + await TelemetryDeck.event("purchase", parameters: ["item": "widget"]) + ``` + +### Event methods are async + +All event-sending methods now require `await`: + +```swift +await TelemetryDeck.event("myEvent") +await TelemetryDeck.navigationPathChanged(from: "home", to: "settings") +await TelemetryDeck.errorOccurred(id: "ParseError") +await TelemetryDeck.purchaseCompleted(transaction: transaction) +await TelemetryDeck.setUserIdentifier("user@example.com") +``` + +In SwiftUI view bodies and `@MainActor` contexts, wrap calls in a `Task`: + +```swift +Button("Tap") { + Task { + await TelemetryDeck.event("buttonTapped") + } +} +``` + +### Configuration properties moved to initialize parameters + +Properties that were set on `TelemetryDeck.Config` in V2 are now parameters of the `initialize` function: + +=== "Before (V2)" + + ```swift + let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") + config.defaultSignalPrefix = "App." + config.defaultParameterPrefix = "MyApp." + config.defaultParameters = {["theme": "dark"]} + TelemetryDeck.initialize(config: config) + ``` + +=== "After (V3)" + + ```swift + try await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE", + eventPrefix: "App.", + parameterPrefix: "MyApp.", + defaultParameters: ["theme": "dark"] + ) + ``` + +### Typed parameters + +Parameters are no longer `[String: String]`. The new `EventParameters` type accepts `String`, `Bool`, `Int`, `Double`, `UUID`, and `Date` values: + +=== "Before (V2)" + + ```swift + TelemetryDeck.signal("event", parameters: [ + "count": "\(items.count)", + "premium": isPremium ? "true" : "false" + ]) + ``` + +=== "After (V3)" + + ```swift + await TelemetryDeck.event("event", parameters: [ + "count": items.count, + "premium": isPremium + ]) + ``` + +The old `[String: String]` overloads still compile but are deprecated. + +### Duration methods renamed + +=== "Before (V2)" + + ```swift + TelemetryDeck.startDurationSignal("loading") + TelemetryDeck.stopAndSendDurationSignal("loading") + TelemetryDeck.cancelDurationSignal("loading") + ``` + +=== "After (V3)" + + ```swift + await TelemetryDeck.startDurationEvent("loading") + await TelemetryDeck.stopAndSendDurationEvent("loading") + await TelemetryDeck.cancelDurationEvent("loading") + ``` + +### updateDefaultUserID → setUserIdentifier + +=== "Before (V2)" + + ```swift + TelemetryDeck.updateDefaultUserID(to: "user@example.com") + ``` + +=== "After (V3)" + + ```swift + await TelemetryDeck.setUserIdentifier("user@example.com") + ``` + +## Removed APIs + +| Removed | Replacement | +|---|---| +| `TelemetryManager.shared` | Use static methods on `TelemetryDeck` | +| `TelemetryClient` (typealias) | `TelemetryDeck` | +| `TelemetryManagerConfiguration` | `TelemetryDeck.Config` or `initialize(appID:namespace:...)` | +| `Provider` protocol | `EventProcessor` protocol | +| `configuration.telemetryAllowDebugBuilds` | `testMode` parameter in `initialize` | + +## Providers → Processors + +The V2 provider system (`register`, `stop`, `enrich`, `transform`) is replaced by the `EventProcessor` protocol. The new model is a middleware chain where each processor calls `next` to pass control downstream. + +If you wrote custom providers, see [Writing Custom Processors](/articles/swift-custom-processors/) for the new approach. + +## Data migration + +The SDK includes an automatic `V2DataMigrator` that converts V2 session data (stored in UserDefaults) to the V3 format on first launch. No action needed — your users' session continuity and retention metrics are preserved. + +## Payload format + +Event payloads are now typed JSON. Values are sent as native `bool`, `int`, `double`, or `string`. V2 sent everything as strings. If you have server-side code parsing payloads, update it to handle typed values. diff --git a/docs/guides/swift-setup.md b/docs/guides/swift-setup.md index 5e96b6d..752039d 100644 --- a/docs/guides/swift-setup.md +++ b/docs/guides/swift-setup.md @@ -6,93 +6,69 @@ tags: - macOS - watchOS - tvOS + - visionOS featured: true -testedOn: Xcode 16 & Swift 5.9 -description: Configure the TelemetryDeck SDK in Your Swift Application for iOS, macOS, watchOS and tvOS -lead: Let's include the TelemetryDeck Swift Package in your application and send events! +testedOn: Xcode 26 & Swift 6.2 & SwiftSDK 3.0.0 +description: Configure the TelemetryDeck SDK in Your Swift Application for iOS, macOS, watchOS, tvOS, and visionOS +lead: Get the TelemetryDeck Swift SDK into your application and start sending events. order: 100 --- ## Prerequisites -This guide assumes you have already created a TelemetryDeck account. If you haven't yet, please [sign up now](https://dashboard.telemetrydeck.com/register)! +- A TelemetryDeck account. [Sign up here](https://dashboard.telemetrydeck.com/register) if you haven't yet. +- Your app's **unique identifier** and **namespace** from the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com). +- Xcode 26 or later, Swift 6.2 or later. +- Minimum deployment targets: iOS 15, macOS 12, watchOS 8, tvOS 15, or visionOS 1. ## Installing the Swift Package -The TelemetryDeck Swift package uses Swift Package Manager. +The TelemetryDeck Swift SDK is distributed via Swift Package Manager. -1. Open Xcode and navigate to the project you want to add TelemetryDeck to -1. In the menu, select File -> Add Package Dependencies.... This will open the Swift Package Manager view -1. Paste `https://github.com/TelemetryDeck/SwiftSDK` into the search field. +1. Open your project in Xcode +1. Select FileAdd Package Dependencies... +1. Paste `https://github.com/TelemetryDeck/SwiftSDK` into the search field 1. Select the `SwiftSDK` package that appears in the list 1. Set the Dependency Rule to Up to Next Major Version -1. Press Add Package to continue +1. Click Add Package ![A screenshot of Xcode adding the TelemetryDeck Package](/assets/xcode-swift-package1.png) -This will include the TelemetryDeck Swift SDK into your app by downloading the source code. Feel free to browse the client's source code, it's tiny and you'll see for yourself how TelemetryDeck is hashing user identifiers before they ever reach the server. Privacy, yay! +## Linking the package -**Watch our [Quick Start video](https://www.youtube.com/watch?v=FA71bSnK_B8) to setup TelemetryDeck in 4 Minutes!** - -[![TelemetryDeck Setup in 4 Minutes – Swift SDK Integration](/assets/yt-4-minute-setup.png)](https://www.youtube.com/watch?v=FA71bSnK_B8) - -## Including the package in your target - -Xcode will ask you to link the package with your target in the next screen, titled Choose Package Products for SwiftSDK. Set the Add to target column to your app target for TelemetryDeck (not "TelemetryClient", which is deprecated) and click Add Package to complete the integration. +Xcode will ask you to link the package with your target. In the screen titled Choose Package Products for SwiftSDK, set the Add to target column to your app target for TelemetryDeck and click Add Package. ![A screenshot of Xcode setting the target for the TelemetryDeck library](/assets/xcode-swift-package2.png) -{% noteinfo "Link Library with more than one Target" %} - -In case Xcode forgets to ask you to link the library with your target, you can do so manually by selecting your target in the project navigator and selecting the Build Phases tab. Click the + button in the Link Binary With Libraries section and select the `TelemetryDeck` library. -{% endnoteinfo %} - -## Initializing the TelemetryDeck Swift package +!!! note "Multiple targets" -The `TelemetryDeck` package will provide you with a class `TelemetryDeck` that you'll use for all interactions with TelemetryDeck. Before you can use that class, you'll need to initialize it at the start of your app. We strongly recommend doing so as soon as possible, as you won't be able to send events before the `TelemetryDeck` is initialized. + If Xcode doesn't prompt you, add it manually: select your target → Build PhasesLink Binary With Libraries+ → select `TelemetryDeck`. -This is slightly different depending on whether you use SwiftUI or UIKit's `AppDelegate` to manage your app's lifecycle, so let's look at these individually. +## Initializing TelemetryDeck -{% noteinfo "You need your app's Unique Identifier" %} -TelemetryDeck assigns a unique identifier to your app, and you need to hand that identifier to the TelemetryDeck SDK. +Initialize TelemetryDeck as early as possible in your app's lifecycle. Events sent before initialization completes are buffered and delivered automatically once it's ready. -Use the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) to create a new app and copy its unique identifier into your computer's clipboard. -{% endnoteinfo %} +!!! note "You need your app's unique identifier and namespace" -You only need **one** way of initializing the TelemetryDeck SDK, either SwiftUI/SceneKit or AppDelegate. If your app is new, you'll likely want to use SwiftUI/SceneKit. + Both values are available in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) under your app's settings. -### Initialization with SwiftUI +Pick whichever lifecycle approach your app uses — SwiftUI or AppDelegate. -For Scene-based SwiftUI applications, we recommend adding the initialization to your `@main` App struct! Open `YourAppNameApp.swift` and look for code that looks like this: - -```swift -import SwiftUI - -@main -struct YourAppNameApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} -``` - -This is the entry point to your app. Now let's add the initialization here. - -Import the TelemetryDeck Package by adding `import TelemetryDeck`. Then add an `init()` method to your App struct that creates a `TelemetryDeck.Config` instance and hands it to `TelemetryDeck.initialize(config:)`, using the **Unique Identifier of your app** that you copied into your clipboard earlier. If you don't have that anymore, you can get it at any time from the TelemetryDeck Dashboard. - -Your code should now look like this: +### SwiftUI ```swift import SwiftUI import TelemetryDeck @main -struct YourAppNameApp: App { +struct YourApp: App { init() { - let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") - TelemetryDeck.initialize(config: config) + Task { + try? await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + } } var body: some Scene { @@ -103,165 +79,153 @@ struct YourAppNameApp: App { } ``` -If you prefer to have it on a single line, you can also write: - -```swift -TelemetryDeck.initialize(config: .init(appID: "YOUR-APP-ID")) -``` - -Your app is now ready to use TelemetryDeck. You can skip the next section which explains setup for UIKit-based apps. - -### Initialization in an AppDelegate based app - -If you use `AppDelegate` to manage your app's life cycle, open the file `AppDelegate.swift` and look for the method `application(:didFinishLaunchingWithOptions:)`. It will probably look similar to this: +### AppDelegate ```swift import UIKit +import TelemetryDeck @main class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + Task { + try? await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + } return true } - - // ... } ``` -By default, Xcode even adds a comment here to tell you where to add stuff that should happen right after launch. +!!! warning "Test mode" -Now, import the `TelemetryDeck` package and configure the `TelemetryDeck` using the **Unique Identifier of your app** that you copied into your clipboard earlier. If you don't have that anymore you can get it at any time from the TelemetryDeck Dashboard. + When running from Xcode in `DEBUG` configuration, events are automatically tagged as **test signals**. Enable **Test Mode** in the TelemetryDeck Dashboard to see them. -```swift -import UIKit -import TelemetryDeck +## Verify your setup -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { +Run your app. The SDK sends a `TelemetryDeck.Session.started` event automatically on launch. Open the TelemetryDeck Dashboard → "Explore > Recent Signals" with Test Mode enabled — you should see it appear. - let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") - TelemetryDeck.initialize(config: config) +That's it. Your app is sending analytics. You can ship this and start getting insights from real users. - return true - } - // ... -} -``` +--- -## Verify your setup +## Sending custom events -Run your app to verify that TelemetryDeck is properly integrated. The SDK automatically sends a `TelemetryDeck.Session.started` signal when your app launches. +Built-in session tracking covers the basics. Custom events let you answer app-specific questions: which features are popular, where users drop off, what settings they prefer. -{% notewarning "When running from Xcode, you're sending test events" %} +```swift +await TelemetryDeck.event("Oven.startBaking") +``` -If your app is built in `DEBUG` configuration (i.e. running from Xcode), your events will be tagged as **Test Signals**, meaning that you can easily filter them out later. You'll see them show up in the TelemetryDeck Dashboard when the **Test Mode** toggle under the tab bar is turned on. -{% endnotewarning %} +Add parameters for more context: + +```swift +await TelemetryDeck.event( + "Oven.startBaking", + parameters: [ + "pizzaMode": dataStore.pizzaMode.isActive, + "temperature": dataStore.targetTemperature + ] +) +``` -Open the TelemetryDeck Dashboard, navigate to "Explore > Recent Signals" and make sure "Test Mode" is enabled. You should see the automatic session signal appear after launching your app. +Parameters accept `String`, `Bool`, `Int`, `Double`, `UUID`, and `Date` values. They're transmitted as native JSON types. ---- +!!! note "Privacy" -{% noteinfo "Ready for basic insights" %} -Congratulations! With just the SDK initialization, TelemetryDeck will automatically track user sessions, app launches, and device information. This basic setup provides valuable built-in insights without any additional code. + The `customUserID` value is SHA256-hashed before transmission. Parameter values are **not** hashed — don't include personally identifiable information in parameters. -You can now build and ship your app. Once users start using it, your TelemetryDeck dashboard will begin showing data about user behavior, device types, and other key metrics. -{% endnoteinfo %} +## User identification -## Enhancing your analytics (optional) +TelemetryDeck auto-generates an anonymous user identifier per installation. If you have your own user identity (email, account ID), set it after login: -While basic session tracking provides valuable information, sending custom events lets you answer questions specific to how users engage with *your* app. +```swift +await TelemetryDeck.setUserIdentifier("user@example.com") +``` -### Common questions you can answer with custom events +The identifier is hashed before it leaves the device. For details on how the default identifier is resolved per platform, cross-device tracking, and server-side usage, see [User Identification](/articles/swift-user-identification/). -- Which features do users engage with most frequently? -- Where in the onboarding flow do users drop off? -- How are users navigating between different screens? -- Which settings or configurations are most popular? +## Configuration options -### Sending custom events +The `initialize` function accepts several optional parameters: -{% noteinfo "What is a signal?" %} -Signals are an indication that **an event** happened in your app, which is used by a **user**. Signals consist of these parts: +```swift +try await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE", + salt: "optional-extra-hash-salt", + defaultUser: "optional-default-user", + testMode: nil, + eventPrefix: "MyApp.", + parameterPrefix: "MyApp.", + defaultParameters: [ + "theme": UserDefaults.standard.string(forKey: "theme") ?? "default", + "tier": isPremium ? "premium" : "free" + ] +) +``` -- **Signal Name** – A string that indicates which kind of event happened -- **User Identifier** – A string that identifies your user (we auto-generate one for you) -- **Optional Parameters** – A dictionary of additional data about your app or the event triggering the signal +| Parameter | Description | +|---|---| +| `salt` | Extra string mixed into user ID hashing for additional privacy | +| `defaultUser` | User identifier set at startup | +| `testMode` | Override automatic test mode detection (`true`/`false`/`nil` for auto) | +| `eventPrefix` | Automatically prepended to all event names | +| `parameterPrefix` | Automatically prepended to all custom parameter keys | +| `sendSessionStartedEvent` | Whether to fire `TelemetryDeck.Session.started` on each new session (default: `true`) | +| `defaultParameters` | Key-value pairs included with every event | -See the [Signals Reference](/docs/api/signals-reference/) for more information about how you can effectively use Signals. -{% endnoteinfo %} +## Disabling analytics -Let's imagine your app is a pizza oven monitor and we want to send a signal that tells us the user has tapped the "Start Baking" button. Go to the place in your code where the user taps the button and add the following code: +Let users opt out: ```swift -TelemetryDeck.signal("Oven.Bake.startBaking") +await TelemetryDeck.setAnalyticsDisabled(true) ``` -That's all you need to send a signal. You do not need to keep an instance of TelemetryDeck and hand it around, just call the static `signal` function on the class directly. If you want to add custom parameters, add them to the function call like this: +Check the current state: ```swift -TelemetryDeck.signal( - "Oven.Bake.startBaking", - parameters: [ - "numberOfTimesPizzaModeHasActivated": "\(dataStore.pizzaMode.count)", - "pizzaCheeseMode": "\(dataStore.pizzaCheeseMode)" - ] -) +let disabled = await TelemetryDeck.isAnalyticsDisabled ``` -{% noteinfo "Privacy Note" %} -The value you pass to `customUserID` will be automatically hashed before being sent to our servers to protect the users privacy. This does not happen for the values in `parameters` though, so hash yourself where needed. -{% endnoteinfo %} +## Shutting down -## Configuring default signal properties (optional) - -When initializing TelemetryDeck, you can configure some defaults to help keep your signals organized and consistent: +If you need to cleanly shut down the SDK (for example before an app extension terminates): ```swift -let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") - -// Add a prefix to all signal names -config.defaultSignalPrefix = "App." -// With this set, calling signal("launched") will actually send "App.launched" - -// Add a prefix to all parameter names -config.defaultParameterPrefix = "MyApp." -// This prefixes all keys in your parameters dictionary - -// Set parameters that will be included with every signal -config.defaultParameters = {[ - "theme": UserDefaults.standard.string(forKey: "theme") ?? "default", - "isPayingUser": FreemiumKit.shared.hasPurchased ? "true" : "false", -]} -// These parameters will be merged with any additional parameters you specify in signal() calls +await TelemetryDeck.terminate() ``` +This flushes pending events, persists the cache, and stops all processors. + ## App Store requirements -Before uploading your app to the App Store, you'll need to complete Apple's privacy details on App Store Connect. Although TelemetryDeck is privacy-focused, you still need to disclose analytics usage. - -For guidance on completing these requirements, see our [Apple App Privacy guide](/docs/articles/apple-app-privacy/). For privacy policy recommendations, check our [Privacy FAQ](/docs/guides/privacy-faq/#do-i-need-to-add-telemetrydeck-to-my-privacy-policy%3F). - -## What to do next - -Now that you've integrated TelemetryDeck, learn how to use the analytics platform to gain valuable insights about your users: - -
-
-
- -
-

- - 📊 Analytics Walkthrough -

-

Learn how to navigate TelemetryDeck, interpret insights, and use analytics to make data-driven decisions that improve your app and grow your user base.

-

- Start here to get real value from your analytics - -

-
-
-
+Apple requires you to disclose analytics usage in App Store Connect, even for privacy-focused tools like TelemetryDeck. See our [Apple App Privacy guide](/articles/apple-app-privacy/) and [Privacy FAQ](/guides/privacy-faq/#do-i-need-to-add-telemetrydeck-to-my-privacy-policy%3F). + +## What's next + +
+ +- **[Processors](/articles/swift-processors/)** + + Learn how events flow through the processor pipeline and what data is automatically collected. + +- **[Custom Processors](/articles/swift-custom-processors/)** + + Build your own processors to add domain-specific metadata to every event. + +- **[Analytics Walkthrough](/basics/index)** + + Navigate TelemetryDeck, interpret insights, and make data-driven decisions. + +- **[Migrating from V2](/guides/swift-migration-v3/)** + + Upgrading from SwiftSDK 2.x? Here's what changed. +
diff --git a/docs/integrations/revenuecat.md b/docs/integrations/revenuecat.md index 022d834..446bc1f 100644 --- a/docs/integrations/revenuecat.md +++ b/docs/integrations/revenuecat.md @@ -3,17 +3,15 @@ title: Using TelemetryDeck with RevenueCat tags: - how-to - iOS -testedOn: RevenueCat Swift SDK 5.2.3 -description: Here's how to integrate TelemetryDeck with RevenueCat to combine usage data with purchase data. -lead: Here's how to integrate TelemetryDeck with RevenueCat to combine usage data with purchase data. +testedOn: RevenueCat Swift SDK 5.2.3, SwiftSDK 3.0.0 +description: Integrate TelemetryDeck with RevenueCat to combine usage data with purchase data. +lead: Integrate TelemetryDeck with RevenueCat to combine usage data with purchase data. order: 90 --- -[RevenueCat](https://www.revenuecat.com/) is a service that helps you process payments and in-app purchases on iOS, Android, and the web. It's a great way to add monetization to your app. +[RevenueCat](https://www.revenuecat.com/) helps you process payments and in-app purchases across iOS, Android, and the web. -RevenueCat pairs excellently with TelemetryDeck – use TelemetryDeck to improve your users' flow through your application, and then present them with convenient In-App-Purchase offers. - -With this integration, you can import your RevenueCat events into TelemetryDeck, and see everything on one dashboard. We'll be using RevenueCat's _Webhooks_ feature to pass on their data to TelemetryDeck. +With this integration, RevenueCat events appear in your TelemetryDeck dashboard alongside your app analytics — all on one screen. !!! warning "Read the announcement" @@ -21,79 +19,71 @@ With this integration, you can import your RevenueCat events into TelemetryDeck, ## Installing RevenueCat and TelemetryDeck -First, we have to integrate both TelemetryDeck and RevenueCat into your app. You can find guides for both here: - -1. [Install and set up TelemetryDeck](/docs/guides/swift-setup/) +1. [Install and set up TelemetryDeck](/guides/swift-setup/) 2. [Install and set up RevenueCat](https://www.revenuecat.com/docs/getting-started/installation) ## Configuring the RevenueCat SDK -RevenueCat has a concept of **user attributes**. Our goal is to set two new user attributes for our RevenueCat users that will make TelemetryDeck recognize them as the same users it is already managing. +RevenueCat uses **user attributes** to correlate users across services. Set two attributes so RevenueCat users match their TelemetryDeck identities: -- `$telemetryDeckAppId`: This attribute should be set to your TelemetryDeck App ID, the same one you pass into the TelemetryDeck SDK for initialization. -- `$telemetryDeckUserId`: This attribute needs to be the **already-hashed user identifier** that TelemetryDeck is using. +- `$telemetryDeckAppId`: Your TelemetryDeck App ID +- `$telemetryDeckUserId`: The **already-hashed** user identifier that TelemetryDeck uses -!!! warning "RevenueCat gets the hashed version of the TelemetryDeck User Identifier" +!!! warning "RevenueCat needs the hashed identifier" - While the TelemetryDeck SDK usually takes care of hashing for you, you'll need to extract the identifier after it's been hashed and pass that on to RevenueCat. Only then will the final identifiers in your TelemetryDeck dashboard match up. + TelemetryDeck hashes user identifiers before transmission. You need to extract the hashed value and pass that to RevenueCat, otherwise the identifiers won't match. - If your version of the TelemetryDeck SDK does not expose a function to vend the hashed user identifier, you can hash it yourself using something like `SHA256(user_id + salt)`. + If your SDK version doesn't expose the hashed identifier directly, compute it yourself: `SHA256(user_id + salt)`. ### iOS -Here's how to set up TelemetryDeck and RevenueCat on iOS. The setup process is similar for other platforms. - ```swift -// 1. -// Initialize TelemetryDeck with your app ID +// 1. Initialize TelemetryDeck let telemetrydeckAppID = "AAAAAAAA-BBBB-CCCC-DDDD" -let telemetryDeckConfig = TelemetryDeck.Config( - appID: telemetrydeckAppID, - salt: "MY_SECRET_SALT" // optional but recommended -) -TelemetryDeck.initialize(config: telemetryDeckConfig) - -// 2. -// Manually set a default user for TelemetryDeck -// We're using IFV here, but you can also use -// e.g. an email address or any other identifying property -let myUserID = UIDevice.current - .identifierForVendor?.uuidString - ?? "unknown user" -TelemetryDeck.updateDefaultUserID(to: myUserID) - -// 3. -// Set up RevenueCat with your TelemetryDeck App ID -// and the pre-hashed TelemetryDeck User ID +Task { + try await TelemetryDeck.initialize( + appID: telemetrydeckAppID, + namespace: "YOUR-NAMESPACE", + salt: "MY_SECRET_SALT" + ) + + // 2. Set a user identifier + let myUserID = UIDevice.current + .identifierForVendor?.uuidString + ?? "unknown user" + await TelemetryDeck.setUserIdentifier(myUserID) +} + +// 3. Configure RevenueCat with the hashed TelemetryDeck user ID Purchases.configure(withAPIKey: "my_revenuecat_api_key") + +// Compute the hashed identifier to pass to RevenueCat +// This must match what TelemetryDeck sends to the server +import CryptoKit +let hashedUser = SHA256.hash( + data: Data((myUserID + "MY_SECRET_SALT").utf8) +).map { String(format: "%02x", $0) }.joined() + Purchases.shared.attribution.setAttributes([ - "$telemetryDeckUserId": TelemetryManager.shared - .hashedDefaultUser - ?? "no-user", + "$telemetryDeckUserId": hashedUser, "$telemetryDeckAppId": telemetrydeckAppID ]) ``` -Here's what's going on in the above example - -1. First we set up TelemetryDeck as described in the setup guide. If you already have set up TelemetryDeck, you can leave your setup unchanged. -2. We then manually set a default user for TelemetryDeck. This allows us to later retrieve a hashed version of the user identifier. -3. Finally, we configure RevenueCat and set up the necessary user attributes. - -!!! warning "You need to keep user identifiers in sync" +!!! warning "Keep identifiers in sync" - Whenever you update your TelemetryDeck user identifier, you'll also need to update the user identifier in RevenueCat's `$telemetryDeckUserId` user attribute. + Whenever you update your TelemetryDeck user identifier, also update RevenueCat's `$telemetryDeckUserId` attribute. -## Setting up RevenueCat's TelemetryDeck Integration +## Setting up RevenueCat's TelemetryDeck integration -Now we need to tell RevenueCat to send copies of all events over to TelemetryDeck. We're using a RevenueCat's **TelemetryDeckIntegration** to do this. +Tell RevenueCat to forward events to TelemetryDeck: -- Navigate to your RevenueCat Project -- In the left sidebar, click **Integrations** -- Select **TelemetryDeck** -- Click **Add Integration** to confirm +1. Navigate to your RevenueCat Project +2. In the left sidebar, click **Integrations** +3. Select **TelemetryDeck** +4. Click **Add Integration** ![A screenshot of RevenueCat's TelemetryDeck Integration](/assets/rc-td-1.png) ![A screenshot of RevenueCat's TelemetryDeck Integration](/assets/rc-td-2.png) -And you're done. RevenueCat events should now arrive and be mixed in with your TelemetryDeck signals 🥳 +RevenueCat events will now appear in your TelemetryDeck dashboard. diff --git a/docs/integrations/superwall.md b/docs/integrations/superwall.md index 9d614df..4df04a1 100644 --- a/docs/integrations/superwall.md +++ b/docs/integrations/superwall.md @@ -3,38 +3,32 @@ title: Using TelemetryDeck with Superwall iOS tags: - how-to - iOS -testedOn: SuperWall iOS SDK 3.2.. -description: Here's how to integrate TelemetryDeck with SuperWall to get insights into your paywalls. -lead: Here's how to integrate TelemetryDeck with SuperWall to get insights into your paywalls. +testedOn: SuperWall iOS SDK 3.2 +description: Integrate TelemetryDeck with SuperWall to get insights into your paywalls. +lead: Integrate TelemetryDeck with SuperWall to get insights into your paywalls. order: 100 --- -[Superwall](https://superwall.com/) is a service that lets you experiment and try out different paywalls and monetization strategies. It's a great way to get started with monetization and to get insights into what works and what doesn't. +[Superwall](https://superwall.com/) lets you experiment with different paywalls and monetization strategies. Combined with TelemetryDeck, you can see how users interact with your paywalls. -It pairs excellently with TelemetryDeck, as you can use TelemetryDeck to get insights into how your users interact with your paywalls. Superwall has various hooks that you can use to send events to TelemetryDeck, and this guide will show you how to do that. +Superwall provides hooks that forward events to TelemetryDeck. This guide shows you how to wire them up. ## Installing Superwall and TelemetryDeck -First, we have to integrate both TelemetryDeck and Superwall into your app. You can find guides for both here: +Integrate both SDKs into your app: -1. [Install and setup the TelemetryDeck SDK](/docs/guides/swift-setup/) +1. [Install and set up the TelemetryDeck SDK](/guides/swift-setup/) 2. [Install and set up the Superwall SDK](https://docs.superwall.com/docs/installation-via-spm) -If you've already set up TelemetryDeck or Superwall, you can skip the installation steps. Just make sure that you've set up both SDKs correctly. The order in which you initialize the SDKs doesn't matter. +If you've already set up either SDK, skip that step. The initialization order doesn't matter. ## Creating a Superwall delegate -Superwall allows you to set a delegate that gets notified when a paywall is shown, dismissed, or when a user subscribes. We're going to use this delegate to send events to TelemetryDeck. - -If you want to dive deeper, check out [Superwall's documentation on how to pass information to various analytics providers](https://docs.superwall.com/docs/3rd-party-analytics). - -Create a new Swift file in your project and name it `SuperwallService.swift`. This file will contain the code that sends events to TelemetryDeck. - -Paste the following code into the file: +Create a new file `SuperwallService.swift`: ```swift import SuperwallKit -import TelemetryClient +import TelemetryDeck class SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { @@ -44,22 +38,23 @@ class SuperwallService: SuperwallDelegate { stringifiedParams[param.key] = String(describing: param.value) } - TelemetryDeck.signal(eventInfo.event.description, parameters: stringifiedParams) + Task { + await TelemetryDeck.event( + eventInfo.event.description, + parameters: stringifiedParams + ) + } } } ``` -Because TelemetryDeck only accepts strings as event metadata, this method accepts all data from Superwall, converts it into strings, and hands it off to TelemetryDeck. The TelemetryDeck takes care of queuing and sending the events to TelemetryDeck. - -## Registering the Superwall delegate - -Now that we have a delegate, we have to register it with Superwall. We can do this in the function that you initialize TelemetryDeck and Superwall in. This is usually in your `App` struct or `AppDelegate`. When in doubt, search for `TelemetryDeck.initialize` in your project. +## Registering the delegate -Add the following line to the function, making sure it is below the initialization code for both TelemetryDeck and Superwall: +In the function where you initialize TelemetryDeck and Superwall (usually your `App` struct or `AppDelegate`), add: ```swift let superwallService = SuperwallService() Superwall.shared.delegate = superwallService ``` -This initializes the Superwall delegate and registers it with Superwall. Now, whenever a paywall is shown, dismissed, or a user subscribes, the delegate will be notified and you'll get analytics data in your TelemetryDeck dashboard. +Paywall shown, dismissed, and subscription events will now appear in your TelemetryDeck dashboard. diff --git a/zensical.toml b/zensical.toml index cd9a34a..9df851e 100644 --- a/zensical.toml +++ b/zensical.toml @@ -48,7 +48,13 @@ Copyright © 2026 TelemetryDeck GmbH nav = [ { "Welcome" = "index.md" }, { "SDK Setup" = [ - { "Swift" = "guides/swift-setup.md" }, + { "Swift" = [ + { "Quick Start" = "guides/swift-setup.md" }, + { "Processors" = "articles/swift-processors.md" }, + { "Custom Processors" = "articles/swift-custom-processors.md" }, + { "User Identification" = "articles/swift-user-identification.md" }, + { "Migrating from V2" = "guides/swift-migration-v3.md" }, + ] }, { "Android (Kotlin)" = "guides/android-setup.md" }, { "Flutter" = "guides/flutter-setup.md" }, { "JavaScript" = "guides/javascript-setup.md" },