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..1518d01 --- /dev/null +++ b/.github/workflows/azure-static-web-apps-purple-bush-012fe8010.yml @@ -0,0 +1,60 @@ +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: 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 + - 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 + 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" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..70ed450 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,31 @@ +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 + - name: Generate llms.txt and ship raw markdown + run: python3 scripts/build_llms.py + - uses: actions/upload-pages-artifact@v5 + with: + path: site + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/README.md b/README.md index 292768f..6de2adb 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,20 @@ 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`). + +## 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/articles/duration-signals.md b/articles/duration-signals.md deleted file mode 100644 index 3cc9440..0000000 --- a/articles/duration-signals.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: Track time-based user engagement with Duration Signals -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. ---- - -## What are Duration Signals? - -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: - -```swift -// Start tracking when the activity begins -TelemetryDeck.startDurationSignal("activityName") - -// ... user performs the activity ... - -// Stop tracking and send the signal when the activity ends -TelemetryDeck.stopAndSendDurationSignal("activityName") -``` - -The duration is automatically calculated and included in your signal as `TelemetryDeck.Signal.durationInSeconds`. - -### View Lifecycle Integration - -Duration Signals integrate seamlessly with your view lifecycles in SwiftUI: - -```swift -struct TutorialView: View { - var body: some View { - VStack { - Text("Welcome to the Tutorial!") - } - .onAppear { - TelemetryDeck.startDurationSignal("tutorial") - } - .onDisappear { - TelemetryDeck.stopAndSendDurationSignal("tutorial") - } - } -} -``` - -Both functions also take an optional `parameters` argument where you can pass additional information just like with the `signal` function. - -## Technical Details - -### SDK Requirements - -- Swift SDK: Version 2.7.0 or later -- Kotlin SDK: Version 4.1.0 or later -- Flutter SDK: Version 2.1.0 or later - -### Edge Cases & Limitations - -- **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. - -## Analyzing Duration Data - -Duration data is sent as a numerical value in the `TelemetryDeck.Signal.durationInSeconds` parameter, which opens up several analysis possibilities. - -### Using the Histogram Aggregation - -The histogram aggregation type is perfect for visualizing the distribution of duration data: - -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) - -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) - - ```json - { - "aggregations": [ - { - "fieldName": "TelemetryDeck.Signal.durationInSeconds", - "name": "durationSketch", - "splitPoints": [0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 7.5, 10, 15, 20, 30, 45, 60, 90, 120], - "type": "histogram" - } - ], - "filter": { - "type": "and", - "fields": [ - { - "type": "range", - "column": "TelemetryDeck.Signal.durationInSeconds", - "matchValueType": "DOUBLE", - "lower": "0", - "upper": "120", - "upperOpen": true - }, - { - "dimension": "type", - "type": "selector", - "value": "" - } - ] - }, - "granularity": "all", - "queryType": "timeseries" - } - ``` - -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) - -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]` - - **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 - -### 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: - -```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") - -// etc. -``` - -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. - -### Content Engagement - -Measure how long users engage with different content types to understand what resonates with your audience: - -```swift -// When user opens an article -TelemetryDeck.startDurationSignal("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)", -]) -``` - -### 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: - -```swift -func fetchData() async throws -> Data { - // Since we're potentially on a background thread, await is needed - await TelemetryDeck.startDurationSignal("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" - ]) - - return data - } catch { - await TelemetryDeck.stopAndSendDurationSignal("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/articles/navigation-signals.md b/articles/navigation-signals.md deleted file mode 100644 index 4692b06..0000000 --- a/articles/navigation-signals.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -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 ---- - -{% notewarning "Upcoming Feature" %} - -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 %} - -## 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. - -```json -{ - "appID": "", - "clientUser": "", - "type": "TelemetryDeck.Navigation.pathChanged", - "payload": { - "TelemetryDeck.Navigation.schemaVersion": "1", - "TelemetryDeck.Navigation.identifier": " -> ", - "TelemetryDeck.Navigation.sourcePath": "", - "TelemetryDeck.Navigation.destinationPath": "" - } -} -``` - -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. - -Here's what each parameter should contain: - -| 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. - -Examples: - -- `index` -- `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. - -### `TelemetryDeck.navigationPathChanged(from: , to: )` - -Calling this method will automatically create a navigation signal with the given source and destination. - -### `TelemetryDeck.navigationPathChanged(to: )` - -Calling this method with just a destination will use the previously last used source as the source for the navigation signal. - -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. - -#### SwiftUI Convenience - -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: - -```swift -struct SettingsView: View { - var body: some View { - Form { - NavigationLink("Account") { - AccountSettingsView() - .trackNavigation(path: "settings.account") - } - } - .trackNavigation(path: "settings") - } -} -``` - -The same considerations about consistent application apply - make sure to use the modifier on all navigation destinations to avoid incorrect navigation paths. diff --git a/articles/preset-errors.md b/articles/preset-errors.md deleted file mode 100644 index 05c71e8..0000000 --- a/articles/preset-errors.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Setting Up the 'Errors' Preset to Reduce Churn -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. ---- - -## 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 %} - -## 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) -``` - -### 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: - -```swift -do { - 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 - ) -} -``` - -### Custom Error Types - -For your own error types, conform to the `IdentifiableError` protocol (built into the Swift SDK): - -```swift -enum MyError: String, IdentifiableError { - case fileMissing - case invalidFormat - - var id: String { self.rawValue } -} - -// Then report directly: -TelemetryDeck.errorOccurred(identifiableError: myError) -``` - -### Error Categories - -The Swift SDK provides three built-in error categories for better organization: - -```swift -// Thrown exceptions (parsing errors, I/O errors, permission errors) -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'." -) - -// App state errors (inconsistent navigation, invalid combinations) -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 %} - -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. - -## Manual Signal Construction for Other Platforms - -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. - -### Signal Structure - -**Event name**: `TelemetryDeck.Error.occurred` - -**Required parameter**: - -- `TelemetryDeck.Error.id`: A consistent identifier for grouping similar errors - -**Optional parameters**: - -- `TelemetryDeck.Error.message`: The full error message or description -- `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 - -- **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 - -## Effect on Privacy & App Tracking Transparency - -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. - -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. diff --git a/articles/preset-purchases.md b/articles/preset-purchases.md deleted file mode 100644 index fbeb9cd..0000000 --- a/articles/preset-purchases.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Setting Up the 'Purchases' Preset to Get Live Purchase Data -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. ---- - -## 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. - -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 use these methods to include your purchase data in TelemetryDeck: - -- 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 - -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 %} - -## 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: - -```swift -TelemetryDeck.purchaseCompleted(transaction: transaction) -``` - -That's it! This method automatically: - -- Extracts the price from the transaction -- Converts the currency to USD (using hard-coded exchange rates) -- Determines if it's a subscription or one-time purchase -- 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 %} - -## Using TelemetryDeck with RevenueCat - -If you use [RevenueCat](https://revenuecat.com), you can use our [RevenueCat Setup Guide](/docs/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. - -## Manual Signal Structure 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 %} - -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: - -### Required Fields - -- **Event name**: Must be `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 %} - -### Optional but Recommended Payload Keys - -To get more detailed insights, include these additional 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 - -### 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: - -```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 -) -``` - -## 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. - -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. - -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) diff --git a/articles/telemetry-client.md b/articles/telemetry-client.md deleted file mode 100644 index e46aaea..0000000 --- a/articles/telemetry-client.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: TelemetryDeck Swift Client Reference -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 ---- - -## Include the Swift Client in your Xcode Project - -See the [Swift Guide](/docs/guides/swift-setup/) on how to set up your app to use the TelemetryDeck Swift Client. - -## Initialization - -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) - -```swift -let config = TelemetryDeck.Config(appID: "") -TelemetryDeck.initialize(config: config) -``` - -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. diff --git a/articles/test-mode.md b/articles/test-mode.md deleted file mode 100644 index 528b77b..0000000 --- a/articles/test-mode.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Getting started with 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. -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. ---- - -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! - -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: - -- 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. - -## 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. - -![Screenshot of the dashboard showing the Test Mode toggle in the upper left corner.](/docs/images/test_mode.png) - -## Sending Signals in Test Mode - -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" - -### Manually set Test Mode in Swift SDK - -```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"] -)` -``` - -### Manually set Test Mode in JavaScript SDK - -```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); -} -``` 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 92% rename from api/api-query-from-insight.md rename to docs/api/api-query-from-insight.md index 543927d..36b58b0 100644 --- a/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/api/api-run-query.md b/docs/api/api-run-query.md similarity index 90% rename from api/api-run-query.md rename to docs/api/api-run-query.md index f7e6c04..c4d61de 100644 --- a/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/api/api-token.md b/docs/api/api-token.md similarity index 90% rename from api/api-token.md rename to docs/api/api-token.md index a13152c..c062aeb 100644 --- a/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/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 92% rename from articles/anonymization-how-it-works.md rename to docs/articles/anonymization-how-it-works.md index 5f74634..ab589f2 100644 --- a/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/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 85% rename from articles/apple-app-privacy.md rename to docs/articles/apple-app-privacy.md index 88c4cb1..267bfa3 100644 --- a/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? @@ -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/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 89% rename from articles/create-custom-dashboards.md rename to docs/articles/create-custom-dashboards.md index 0aadfae..8216dc8 100644 --- a/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/articles/decide-to-drop-ios-version.md b/docs/articles/decide-to-drop-ios-version.md similarity index 98% rename from articles/decide-to-drop-ios-version.md rename to docs/articles/decide-to-drop-ios-version.md index ddd9c7b..6380978 100644 --- a/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/articles/documentation.md b/docs/articles/documentation.md similarity index 89% rename from articles/documentation.md rename to docs/articles/documentation.md index 8428599..8c841da 100644 --- a/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 @@ -188,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 new file mode 100644 index 0000000..595f18f --- /dev/null +++ b/docs/articles/duration-signals.md @@ -0,0 +1,189 @@ +--- +title: Duration Events +tags: + - setup + - how-to + - beginner + - swift +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 +--- + +## How it works + +Bracket an activity with two calls — start and stop. The SDK handles timing, background exclusion, and parameter merging automatically. + +```swift +await TelemetryDeck.startDurationEvent("activityName") + +// ... user performs the activity ... + +await TelemetryDeck.stopAndSendDurationEvent("activityName") +``` + +The duration is included as `TelemetryDeck.Signal.durationInSeconds` with millisecond precision (3 decimal places). + +### SwiftUI view lifecycle + +```swift +struct TutorialView: View { + var body: some View { + VStack { + Text("Welcome to the Tutorial!") + } + .onAppear { + Task { await TelemetryDeck.startDurationEvent("tutorial") } + } + .onDisappear { + Task { await TelemetryDeck.stopAndSendDurationEvent("tutorial") } + } + } +} +``` + +Both functions accept optional `parameters` for additional context. + +## SDK requirements + +- Swift SDK: 3.0.0 or later +- Kotlin SDK: 4.1.0 or later +- Flutter SDK: 2.1.0 or later + +## Edge cases + +- **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: + +```swift +await TelemetryDeck.startDurationEvent( + "longRunningTask", + includeBackgroundTime: true +) +``` + +### Cancelling a duration event + +If the activity is abandoned before completion: + +```swift +await TelemetryDeck.cancelDurationEvent("activityName") +``` + +## Analyzing duration data + +Duration data is sent as a numerical value in the `TelemetryDeck.Signal.durationInSeconds` parameter. The histogram aggregation is a natural fit for visualizing distribution. + +### 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. Paste this histogram aggregation query, replacing ``: + + ![A screenshot of the JSON Editor text field](/assets/duration-signal-02.png) + + ```json + { + "aggregations": [ + { + "fieldName": "TelemetryDeck.Signal.durationInSeconds", + "name": "durationSketch", + "splitPoints": [0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 7.5, 10, 15, 20, 30, 45, 60, 90, 120], + "type": "histogram" + } + ], + "filter": { + "type": "and", + "fields": [ + { + "type": "range", + "column": "TelemetryDeck.Signal.durationInSeconds", + "matchValueType": "DOUBLE", + "lower": "0", + "upper": "120", + "upperOpen": true + }, + { + "dimension": "type", + "type": "selector", + "value": "" + } + ] + }, + "granularity": "all", + "queryType": "timeseries" + } + ``` + +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. 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]` + +## Examples + +### Onboarding steps + +```swift +await TelemetryDeck.startDurationEvent("Onboarding.step1") + +// When moving to step 2 +await TelemetryDeck.stopAndSendDurationEvent("Onboarding.step1", parameters: [ + "pushAccess": "granted" +]) +await TelemetryDeck.startDurationEvent("Onboarding.step2") +``` + +Duration events are regular events, so you can reuse them in [funnel charts](/articles/how-to-funnel-insights/). + +### Content engagement + +```swift +await TelemetryDeck.startDurationEvent("Content.viewing", parameters: [ + "contentType": "article", + "contentID": article.id, + "contentCategory": article.category, +]) + +// When leaving the article +await TelemetryDeck.stopAndSendDurationEvent("Content.viewing", parameters: [ + "reachedEnd": userReachedEnd +]) +``` + +### Network request timing + +```swift +func fetchData() async throws -> Data { + await TelemetryDeck.startDurationEvent("Network.fetch", parameters: [ + "endpoint": "users/profile" + ]) + + do { + let (data, response) = try await URLSession.shared.data(from: url) + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 + + await TelemetryDeck.stopAndSendDurationEvent("Network.fetch", parameters: [ + "status": statusCode, + "success": true + ]) + + return data + } catch { + await TelemetryDeck.stopAndSendDurationEvent("Network.fetch", parameters: [ + "success": false + ]) + throw error + } +} +``` 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 94% rename from articles/how-to-funnel-insights.md rename to docs/articles/how-to-funnel-insights.md index 236a704..8a15ccd 100644 --- a/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/articles/insights-about-daily-users.md b/docs/articles/insights-about-daily-users.md similarity index 97% rename from articles/insights-about-daily-users.md rename to docs/articles/insights-about-daily-users.md index c777a80..c55a4ea 100644 --- a/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/articles/insights-about-monthly-users.md b/docs/articles/insights-about-monthly-users.md similarity index 89% rename from articles/insights-about-monthly-users.md rename to docs/articles/insights-about-monthly-users.md index 2e6bcff..94597de 100644 --- a/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/articles/insights-about-referrers.md b/docs/articles/insights-about-referrers.md similarity index 89% rename from articles/insights-about-referrers.md rename to docs/articles/insights-about-referrers.md index f922729..30eb3f6 100644 --- a/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/articles/insights-about-system-version.md b/docs/articles/insights-about-system-version.md similarity index 97% rename from articles/insights-about-system-version.md rename to docs/articles/insights-about-system-version.md index 038fd8e..a826808 100644 --- a/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/articles/insights.md b/docs/articles/insights.md similarity index 94% rename from articles/insights.md rename to docs/articles/insights.md index 4fc8c23..53e7fe6 100644 --- a/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/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 95% rename from articles/login-options.md rename to docs/articles/login-options.md index 0ab72c4..746a8b6 100644 --- a/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/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/docs/articles/navigation-signals.md b/docs/articles/navigation-signals.md new file mode 100644 index 0000000..6b6e182 --- /dev/null +++ b/docs/articles/navigation-signals.md @@ -0,0 +1,80 @@ +--- +title: Navigation Signals +tags: setup +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 +--- + +!!! note "Web Analytics already tracks navigation" + + If you're using TelemetryDeck's Web SDK, navigation is tracked automatically. This guide is for native app SDKs. + +## Format + +A navigation signal has the type `TelemetryDeck.Navigation.pathChanged` with these parameters: + +```json +{ + "appID": "", + "clientUser": "", + "type": "TelemetryDeck.Navigation.pathChanged", + "payload": { + "TelemetryDeck.Navigation.schemaVersion": "1", + "TelemetryDeck.Navigation.identifier": " -> ", + "TelemetryDeck.Navigation.sourcePath": "", + "TelemetryDeck.Navigation.destinationPath": "" + } +} +``` + +| 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. | + +## Navigation paths + +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: + +- `index` +- `settings.user.changePassword` +- `/blog/ios-market-share` + +## Convenience methods + +### With source and destination + +```swift +await TelemetryDeck.navigationPathChanged(from: "home", to: "settings") +``` + +### Destination only + +```swift +await TelemetryDeck.navigationPathChanged(to: "settings") +``` + +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. + +### SwiftUI view modifier + +```swift +struct SettingsView: View { + var body: some View { + Form { + NavigationLink("Account") { + AccountSettingsView() + .trackNavigation(path: "settings.account") + } + } + .trackNavigation(path: "settings") + } +} +``` + +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/articles/notebooks.md b/docs/articles/notebooks.md similarity index 97% rename from articles/notebooks.md rename to docs/articles/notebooks.md index f193311..043d6b8 100644 --- a/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-errors.md b/docs/articles/preset-errors.md new file mode 100644 index 0000000..2f94ff0 --- /dev/null +++ b/docs/articles/preset-errors.md @@ -0,0 +1,116 @@ +--- +title: Tracking Errors +tags: + - setup + - beginner + - insights + - presets +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? + +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 + +```swift +await TelemetryDeck.errorOccurred(id: error.localizedDescription) +``` + +### Better error identification + +`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) +} catch { + 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 + +Conform your own error types to `IdentifiableError`: + +```swift +enum MyError: String, IdentifiableError { + case fileMissing + case invalidFormat + + var id: String { self.rawValue } +} + +await TelemetryDeck.errorOccurred(identifiableError: myError) +``` + +### Error categories + +Three built-in categories for organization: + +```swift +// 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 +await TelemetryDeck.errorOccurred( + id: "ProjectForm.hourlyRateConversionFailed", + category: .userInput, + message: "Text could not be converted to Int" +) + +// App state errors — inconsistent navigation, invalid combinations +await TelemetryDeck.errorOccurred( + id: "NavigationState.invalidTransition", + category: .appState, + message: "Cannot navigate from login to dashboard without authentication" +) +``` + +!!! note + + When using `errorOccurred(identifiableError:)`, the category defaults to `.thrownException`. + +The `errorOccurred` function accepts the same optional arguments as `event` (`parameters`, `floatValue`, `customUserID`). + +## Manual signal construction for other platforms + +If you're not using the Swift SDK, construct the error signal manually. + +**Event name**: `TelemetryDeck.Error.occurred` + +**Required parameter**: + +- `TelemetryDeck.Error.id`: A consistent identifier for grouping similar errors + +**Optional parameters**: + +- `TelemetryDeck.Error.message`: The full error message +- `TelemetryDeck.Error.category`: One of `thrown-exception`, `user-input`, or `app-state` + +## Built-in error categories + +| 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 | + +## Privacy considerations + +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 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 new file mode 100644 index 0000000..5737c08 --- /dev/null +++ b/docs/articles/preset-purchases.md @@ -0,0 +1,103 @@ +--- +title: Tracking Purchases +tags: + - setup + - beginner + - insights + - presets +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? + +App Store Connect purchase data lags by several hours and requires frequent re-authentication. TelemetryDeck gives you purchase data within seconds. + +You can track purchases through: + +- The TelemetryDeck Swift SDK directly +- RevenueCat integration +- FreemiumKit integration + +!!! warning "Live data vs. correct data" + + 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 + +Pass a StoreKit transaction to the convenience method: + +```swift +await TelemetryDeck.purchaseCompleted(transaction: transaction) +``` + +This automatically: + +- Extracts the price from the transaction +- 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 + +!!! 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 + +See our [RevenueCat Setup Guide](/integrations/revenuecat/). + +## Using FreemiumKit + +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 construction for other platforms + +!!! warning + + Only needed if you are NOT using the TelemetryDeck Swift SDK. + +### Required fields + +- **Event name**: `TelemetryDeck.Purchase.completed` +- **`floatValue`**: The purchase amount in USD + +!!! warning "Currency conversion" + + 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. + +### Optional parameters + +- `TelemetryDeck.Purchase.type`: `subscription` or `one-time-purchase` +- `TelemetryDeck.Purchase.countryCode`: Storefront country code +- `TelemetryDeck.Purchase.currencyCode`: Storefront currency code + +### Example + +```swift +let priceInUSD = convertToUSD(transaction.price, from: transaction.currencyCode) + +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 +) +``` + +## Privacy + +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. + +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/articles/set-up-filters-insights.md b/docs/articles/set-up-filters-insights.md similarity index 96% rename from articles/set-up-filters-insights.md rename to docs/articles/set-up-filters-insights.md index b48ad8b..c467918 100644 --- a/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/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/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 new file mode 100644 index 0000000..bd69079 --- /dev/null +++ b/docs/articles/telemetry-client.md @@ -0,0 +1,30 @@ +--- +title: TelemetryClient (Legacy) +tags: + - Swift + - SDK + - 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. +--- + +!!! warning "Outdated" + + 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. + +## Historical context + +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. + +## Quick migration reference + +| 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 new file mode 100644 index 0000000..f3b1d38 --- /dev/null +++ b/docs/articles/test-mode.md @@ -0,0 +1,67 @@ +--- +title: Test Mode +tags: + - setup + - testmode + - quickstart + - beginner +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. +--- + +During development and testing, your app sends events that aren't from real users. Test Mode keeps these separate from production analytics. + +Benefits of sending test signals: + +- 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 + +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) + +## 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 + +### Swift SDK + +Override test mode at initialization: + +```swift +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 +}); +``` + +Or set it per-signal: + +```javascript +td.signal("eventName", { isTestMode: "true" }); +``` diff --git a/articles/update-package.md b/docs/articles/update-package.md similarity index 97% rename from articles/update-package.md rename to docs/articles/update-package.md index b0ceba9..4a6d60e 100644 --- a/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/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/basics/acquisition.md b/docs/basics/acquisition.md similarity index 95% rename from basics/acquisition.md rename to docs/basics/acquisition.md index 2e61e7e..454707b 100644 --- a/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/basics/activation.md b/docs/basics/activation.md similarity index 95% rename from basics/activation.md rename to docs/basics/activation.md index 4642b96..d17191e 100644 --- a/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/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 97% rename from basics/index.md rename to docs/basics/index.md index eb880ad..1791928 100644 --- a/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/basics/metrics.md b/docs/basics/metrics.md similarity index 97% rename from basics/metrics.md rename to docs/basics/metrics.md index f5c438a..04e3d00 100644 --- a/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/basics/pirate-metrics.md b/docs/basics/pirate-metrics.md similarity index 99% rename from basics/pirate-metrics.md rename to docs/basics/pirate-metrics.md index 01b179b..6436068 100644 --- a/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/basics/retention.md b/docs/basics/retention.md similarity index 95% rename from basics/retention.md rename to docs/basics/retention.md index 7e83c31..6abd1d7 100644 --- a/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/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 85% rename from guides/javascript-setup.md rename to docs/guides/javascript-setup.md index e896f9b..691a19a 100644 --- a/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 @@ -87,14 +85,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" %} - -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. +!!! warning "Special treatment for frameworks" -{% 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/guides/objective-c-setup.md b/docs/guides/objective-c-setup.md new file mode 100644 index 0000000..a8fb893 --- /dev/null +++ b/docs/guides/objective-c-setup.md @@ -0,0 +1,92 @@ +--- +title: Objective-C Setup Guide +tags: + - Setup + - iOS + - macOS + - ObjectiveC +featured: false +testedOn: Xcode 26 & SwiftSDK 3.0.0 +description: Configure the TelemetryDeck SDK in Your Objective-C Application for iOS and macOS +lead: Include the TelemetryDeck Swift Package in your Objective-C application and send events. +order: 1000 +--- + +Objective-C apps (or mixed Swift/ObjC projects) can use the TelemetryDeck Swift SDK through Objective-C interop. + +## Installing the package + +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 + +## Linking the package + +In the Choose Package Products for SwiftSDK screen, select the `TelemetryDeck` library and click Add Package. + +!!! note "Multiple targets" + + If Xcode doesn't prompt you, add it manually: select your target → Build PhasesLink Binary With Libraries+ → select `TelemetryDeck`. + +## Initializing TelemetryDeck + +Add initialization to your App Delegate's `application:didFinishLaunchingWithOptions:`: + +```objc +@import TelemetryDeck; + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + [TelemetryDeck initializeWithAppID:@"YOUR-APP-ID" + namespace:@"YOUR-NAMESPACE"]; + + return YES; +} +``` + +!!! note "You need your app's unique identifier and namespace" + + Both values are available in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) under your app's settings. + +## Verify your setup + +Run your app. The SDK automatically sends a `TelemetryDeck.Session.started` event on launch. + +!!! warning "Test signals" + + Events from `DEBUG` builds are tagged as test signals. Enable **Test Mode** in the TelemetryDeck Dashboard to see them. + +Open the Dashboard → "Explore > Recent Signals" with Test Mode enabled. + +--- + +## Sending custom events + +```objc +[TelemetryDeck event:@"pizzaModeActivated"]; +``` + +With parameters: + +```objc +[TelemetryDeck event:@"pizzaModeActivated" + parameters:@{@"cheeseMode": @"extraCheesy"}]; +``` + +## App Store requirements + +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 + +
+ +- **[Analytics Walkthrough](/basics/index)** + + Navigate TelemetryDeck, interpret insights, and make data-driven decisions. + +
diff --git a/guides/privacy-faq.md b/docs/guides/privacy-faq.md similarity index 99% rename from guides/privacy-faq.md rename to docs/guides/privacy-faq.md index 975e825..347089b 100644 --- a/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/guides/react-setup.md b/docs/guides/react-setup.md similarity index 98% rename from guides/react-setup.md rename to docs/guides/react-setup.md index 5ff1982..c7d851e 100644 --- a/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-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 new file mode 100644 index 0000000..752039d --- /dev/null +++ b/docs/guides/swift-setup.md @@ -0,0 +1,231 @@ +--- +title: Swift Setup Guide +tags: + - Setup + - iOS + - macOS + - watchOS + - tvOS + - visionOS +featured: true +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 + +- 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 SDK is distributed via Swift Package Manager. + +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. Click Add Package + +![A screenshot of Xcode adding the TelemetryDeck Package](/assets/xcode-swift-package1.png) + +## Linking the package + +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) + +!!! note "Multiple targets" + + If Xcode doesn't prompt you, add it manually: select your target → Build PhasesLink Binary With Libraries+ → select `TelemetryDeck`. + +## Initializing TelemetryDeck + +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. + +!!! note "You need your app's unique identifier and namespace" + + Both values are available in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) under your app's settings. + +Pick whichever lifecycle approach your app uses — SwiftUI or AppDelegate. + +### SwiftUI + +```swift +import SwiftUI +import TelemetryDeck + +@main +struct YourApp: App { + init() { + Task { + try? await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + } + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +### AppDelegate + +```swift +import UIKit +import TelemetryDeck + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + Task { + try? await TelemetryDeck.initialize( + appID: "YOUR-APP-ID", + namespace: "YOUR-NAMESPACE" + ) + } + return true + } +} +``` + +!!! warning "Test mode" + + When running from Xcode in `DEBUG` configuration, events are automatically tagged as **test signals**. Enable **Test Mode** in the TelemetryDeck Dashboard to see them. + +## Verify your setup + +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. + +That's it. Your app is sending analytics. You can ship this and start getting insights from real users. + +--- + +## Sending custom events + +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. + +```swift +await TelemetryDeck.event("Oven.startBaking") +``` + +Add parameters for more context: + +```swift +await TelemetryDeck.event( + "Oven.startBaking", + parameters: [ + "pizzaMode": dataStore.pizzaMode.isActive, + "temperature": dataStore.targetTemperature + ] +) +``` + +Parameters accept `String`, `Bool`, `Int`, `Double`, `UUID`, and `Date` values. They're transmitted as native JSON types. + +!!! note "Privacy" + + The `customUserID` value is SHA256-hashed before transmission. Parameter values are **not** hashed — don't include personally identifiable information in parameters. + +## User identification + +TelemetryDeck auto-generates an anonymous user identifier per installation. If you have your own user identity (email, account ID), set it after login: + +```swift +await TelemetryDeck.setUserIdentifier("user@example.com") +``` + +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/). + +## Configuration options + +The `initialize` function accepts several optional parameters: + +```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" + ] +) +``` + +| 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 | + +## Disabling analytics + +Let users opt out: + +```swift +await TelemetryDeck.setAnalyticsDisabled(true) +``` + +Check the current state: + +```swift +let disabled = await TelemetryDeck.isAnalyticsDisabled +``` + +## Shutting down + +If you need to cleanly shut down the SDK (for example before an app extension terminates): + +```swift +await TelemetryDeck.terminate() +``` + +This flushes pending events, persists the cache, and stops all processors. + +## App Store requirements + +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/guides/vue-setup.md b/docs/guides/vue-setup.md similarity index 97% rename from guides/vue-setup.md rename to docs/guides/vue-setup.md index 6d58dc8..dba9fd7 100644 --- a/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/guides/web-setup.md b/docs/guides/web-setup.md similarity index 92% rename from guides/web-setup.md rename to docs/guides/web-setup.md index 0a97303..42ee7e9 100644 --- a/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/index.md b/docs/index.md new file mode 100644 index 0000000..80b4bc4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,213 @@ +--- +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. + + +--- + +## 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 + +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/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 94% rename from ingest/v1.md rename to docs/ingest/v1.md index 8a79b77..913b9c3 100644 --- a/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/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/docs/integrations/revenuecat.md b/docs/integrations/revenuecat.md new file mode 100644 index 0000000..446bc1f --- /dev/null +++ b/docs/integrations/revenuecat.md @@ -0,0 +1,89 @@ +--- +title: Using TelemetryDeck with RevenueCat +tags: + - how-to + - iOS +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/) helps you process payments and in-app purchases across iOS, Android, and the web. + +With this integration, RevenueCat events appear in your TelemetryDeck dashboard alongside your app analytics — all on one screen. + +!!! 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. + +## Installing RevenueCat and TelemetryDeck + +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 uses **user attributes** to correlate users across services. Set two attributes so RevenueCat users match their TelemetryDeck identities: + +- `$telemetryDeckAppId`: Your TelemetryDeck App ID +- `$telemetryDeckUserId`: The **already-hashed** user identifier that TelemetryDeck uses + +!!! warning "RevenueCat needs the hashed identifier" + + 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 SDK version doesn't expose the hashed identifier directly, compute it yourself: `SHA256(user_id + salt)`. + +### iOS + +```swift +// 1. Initialize TelemetryDeck +let telemetrydeckAppID = "AAAAAAAA-BBBB-CCCC-DDDD" +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": hashedUser, + "$telemetryDeckAppId": telemetrydeckAppID +]) +``` + +!!! warning "Keep identifiers in sync" + + Whenever you update your TelemetryDeck user identifier, also update RevenueCat's `$telemetryDeckUserId` attribute. + +## Setting up RevenueCat's TelemetryDeck integration + +Tell RevenueCat to forward events to TelemetryDeck: + +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) + +RevenueCat events will now appear in your TelemetryDeck dashboard. diff --git a/docs/integrations/superwall.md b/docs/integrations/superwall.md new file mode 100644 index 0000000..4df04a1 --- /dev/null +++ b/docs/integrations/superwall.md @@ -0,0 +1,60 @@ +--- +title: Using TelemetryDeck with Superwall iOS +tags: + - how-to + - iOS +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/) lets you experiment with different paywalls and monetization strategies. Combined with TelemetryDeck, you can see how users interact with your paywalls. + +Superwall provides hooks that forward events to TelemetryDeck. This guide shows you how to wire them up. + +## Installing Superwall and TelemetryDeck + +Integrate both SDKs into your app: + +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 either SDK, skip that step. The initialization order doesn't matter. + +## Creating a Superwall delegate + +Create a new file `SuperwallService.swift`: + +```swift +import SuperwallKit +import TelemetryDeck + +class SuperwallService: SuperwallDelegate { + func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { + var stringifiedParams: [String: String] = [:] + + for param in eventInfo.params { + stringifiedParams[param.key] = String(describing: param.value) + } + + Task { + await TelemetryDeck.event( + eventInfo.event.description, + parameters: stringifiedParams + ) + } + } +} +``` + +## Registering the delegate + +In the function where you initialize TelemetryDeck and Superwall (usually your `App` struct or `AppDelegate`), add: + +```swift +let superwallService = SuperwallService() +Superwall.shared.delegate = superwallService +``` + +Paywall shown, dismissed, and subscription events will now appear in your TelemetryDeck dashboard. 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 88% rename from tql/aggregators.md rename to docs/tql/aggregators.md index fddc53d..5be6e2b 100644 --- a/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 { @@ -117,11 +115,9 @@ Also, you can use post aggregators to do union, intersection or difference on sk `cardinality` computes the cardinality of a dimension. -{% notewarning "Deprecated" %} - -This aggregator is deprecated. Use `thetaSketch` instead. +!!! warning "Deprecated" -{% endnotewarning %} + This aggregator is deprecated. Use `thetaSketch` instead. ```json { 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 80% rename from tql/dimensionSpec.md rename to docs/tql/dimensionSpec.md index 28cd830..1f8b1af 100644 --- a/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/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 94% rename from tql/funnels.md rename to docs/tql/funnels.md index 5b7d9b5..2aca4ad 100644 --- a/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. @@ -114,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?" %} - -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. +!!! warning "What's a theta sketch?" -{% 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/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 92% rename from tql/query.md rename to docs/tql/query.md index 292ad82..2119a4e 100644 --- a/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: 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 diff --git a/guides/objective-c-setup.md b/guides/objective-c-setup.md deleted file mode 100644 index 3693579..0000000 --- a/guides/objective-c-setup.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: Objective-C Setup Guide -tags: - - Setup - - iOS - - macOS - - watchOS - - tvOS - - ObjectiveC -featured: false -testedOn: Xcode 14.1 & Swift 5.5 -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! -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. - -## Installing the package - -The TelemetryDeck Swift package uses Swift Package Manager. - -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. - -![A screenshot of Xcode adding the TelemetryDeck Package](/docs/images/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! - -## Including the package in your target - -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. - -{% 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: - -```objc -// Import the TelemetryClient library -@import TelemetryClient; - -// ... - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - // Your other initialization code here ... - - // 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"]; - - 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. - -Use the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) to create a new app and copy its unique identifier into your computer's clipboard. -{% endnoteinfo %} - -## 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: - -```objc -[TelemetryManager send:@"applicationDidFinishLaunching"]; -``` - -{% 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 %} - -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. - ---- - -{% 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) - -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 - -{% 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: - -- **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 - -See the [Signals Reference](/docs/api/signals-reference/) for more information about how you can effectively use Signals. -{% endnoteinfo %} - -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: - -```objc -[ - TelemetryManager - send:@"applicationDidFinishLaunching" - with:@{@"pizzaCheeseMode": @"extraCheesy"} -]; -``` - -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). - -## 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 - -

-
-
-
-
diff --git a/guides/swift-setup.md b/guides/swift-setup.md deleted file mode 100644 index 9af5d71..0000000 --- a/guides/swift-setup.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: Swift Setup Guide -tags: - - Setup - - iOS - - macOS - - watchOS - - tvOS -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! -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)! - -## Installing the Swift Package - -The TelemetryDeck Swift package uses 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. 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 - -![A screenshot of Xcode adding the TelemetryDeck Package](/docs/images/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) - -## 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) - -{% 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 - -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. - -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. - -{% 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. - -Use the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com) to create a new app and copy its unique identifier into your computer's clipboard. -{% endnoteinfo %} - -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. - -### Initialization with SwiftUI - -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: - -```swift -import SwiftUI -import TelemetryDeck - -@main -struct YourAppNameApp: App { - init() { - let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") - TelemetryDeck.initialize(config: config) - } - - var body: some Scene { - WindowGroup { - ContentView() - } - } -} -``` - -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: - -```swift -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // ... -} -``` - -By default, Xcode even adds a comment here to tell you where to add stuff that should happen right after launch. - -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. - -```swift -import UIKit -import TelemetryDeck - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - let config = TelemetryDeck.Config(appID: "YOUR-APP-ID") - TelemetryDeck.initialize(config: config) - - return true - } - // ... -} -``` - -## Verify your setup - -Run your app to verify that TelemetryDeck is properly integrated. The SDK automatically sends a `TelemetryDeck.Session.started` signal when your app launches. - -{% notewarning "When running from Xcode, you're sending test events" %} - -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 %} - -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. - ---- - -{% 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) - -While basic session tracking provides valuable information, sending custom events lets you answer questions specific to how users engage with *your* app. - -### Common questions you can answer with custom events - -- 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? - -### Sending custom events - -{% 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: - -- **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 - -See the [Signals Reference](/docs/api/signals-reference/) for more information about how you can effectively use Signals. -{% endnoteinfo %} - -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: - -```swift -TelemetryDeck.signal("Oven.Bake.startBaking") -``` - -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: - -```swift -TelemetryDeck.signal( - "Oven.Bake.startBaking", - parameters: [ - "numberOfTimesPizzaModeHasActivated": "\(dataStore.pizzaMode.count)", - "pizzaCheeseMode": "\(dataStore.pizzaCheeseMode)" - ] -) -``` - -{% 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 %} - -## Configuring default signal properties (optional) - -When initializing TelemetryDeck, you can configure some defaults to help keep your signals organized and consistent: - -```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 -``` - -## 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 - -

-
-
-
-
diff --git a/index.njk b/index.njk deleted file mode 100644 index 398e42f..0000000 --- a/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.

diff --git a/integrations/revenuecat.md b/integrations/revenuecat.md deleted file mode 100644 index bf80a5a..0000000 --- a/integrations/revenuecat.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -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. -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 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. - -{% noteinfo "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 %} - -## 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/) -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. - -- `$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. - -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)`. - -{% endnoteinfo %} - -### 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 -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 -Purchases.configure(withAPIKey: "my_revenuecat_api_key") -Purchases.shared.attribution.setAttributes([ - "$telemetryDeckUserId": TelemetryManager.shared - .hashedDefaultUser - ?? "no-user", - "$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. - -{% notewarning "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 %} - -## 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. - -- Navigate to your RevenueCat Project -- In the left sidebar, click **Integrations** -- 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) - -And you're done. RevenueCat events should now arrive and be mixed in with your TelemetryDeck signals 🥳 diff --git a/integrations/superwall.md b/integrations/superwall.md deleted file mode 100644 index 9d614df..0000000 --- a/integrations/superwall.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -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. -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. - -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. - -## Installing Superwall and TelemetryDeck - -First, we have to integrate both TelemetryDeck and Superwall into your app. You can find guides for both here: - -1. [Install and setup the TelemetryDeck SDK](/docs/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. - -## 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: - -```swift -import SuperwallKit -import TelemetryClient - -class SuperwallService: SuperwallDelegate { - func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { - var stringifiedParams: [String: String] = [:] - - for param in eventInfo.params { - stringifiedParams[param.key] = String(describing: param.value) - } - - TelemetryDeck.signal(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. - -Add the following line to the function, making sure it is below the initialization code for both TelemetryDeck and Superwall: - -```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. 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/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" + } +} diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..9df851e --- /dev/null +++ b/zensical.toml @@ -0,0 +1,484 @@ +# ============================================================================ +# +# 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] + + +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 +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. +# 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 = [ + { "Welcome" = "index.md" }, + { "SDK Setup" = [ + { "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" }, + { "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 +# 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 = "assets/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.svg" + +# ---------------------------------------------------------------------------- +# 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]] +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 system preference" +primary = "orange" +accent = "deep orange" + +# ---------------------------------------------------------------------------- +# The "extra" section contains miscellaneous settings. +# ---------------------------------------------------------------------------- +#[[project.extra.social]] +#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, +# 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]