diff --git a/README.md b/README.md index 862aea1451..b4634f6ed9 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Within this file you'll need to add a Variant block, followed by the API informa | --------- | ----------- | ------- | | `{PLATFORM_NAME}` | The name of the platform for the API. This **must** match the name within the previously written query. | `android` | | `{API_NAME}` | The name of the API. This **must** match the name within the previously written query. | `get-identities` | -| `{PARAGRAPH_NUMBERS}` | The number of paragraphs within the API blurb. Please note that **each new line counts as a new paragraph**. Additionally, a code block and a list count only as **one** paragraph, despite how long a code block or list can be. If you are unsure about what number to put here, make sure you run the documentation site locally and ensure it renders properly. | +| `{PARAGRAPH_NUMBERS}` | The number of paragraphs within the API blurb. Please note that **each new line counts as a new paragraph**. Additionally, a code block and a list count only as **one** paragraph, despite how long a code block or list can be. If you are unsure about what number to put here, make sure you run the documentation site locally and ensure it renders properly. | `3` | An full example of a Variant block with the API information can be seen below: diff --git a/gatsby-config.js b/gatsby-config.js index 2f581e25fa..9f6a6ddd5f 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -1137,6 +1137,31 @@ module.exports = { } ] }, + { + title: "Adobe Content Analytics", + path: "/solution/adobe-content-analytics/", + pages: [{ + title: "Overview", + path: "/solution/adobe-content-analytics/" + }, + { + title: "API reference", + path: "/solution/adobe-content-analytics/api-reference" + }, + { + title: "Experience tracking", + path: "/solution/adobe-content-analytics/experience-tracking" + }, + { + title: "Advanced configuration", + path: "/solution/adobe-content-analytics/advanced-configuration" + }, + { + title: "Crash recovery", + path: "/solution/adobe-content-analytics/crash-recovery" + } + ] + }, { title: "Adobe Audience Manager", path: "/solution/adobe-audience-manager/", @@ -1208,7 +1233,6 @@ module.exports = { } ] }, - { title: "Adobe Media Analytics", path: "/solution/adobe-media-analytics", diff --git a/src/pages/solution/adobe-content-analytics/advanced-configuration.md b/src/pages/solution/adobe-content-analytics/advanced-configuration.md new file mode 100644 index 0000000000..33b637eb36 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/advanced-configuration.md @@ -0,0 +1,205 @@ +--- +title: Content Analytics Advanced Configuration +description: Learn how to configure advanced features for Content Analytics. +keywords: +- Adobe Analytics +- Product overview +--- +import Tabs from './tabs/advanced-configuration.md' +import InitializeSDK from '/src/pages/resources/initialize.md' + +# Advanced Configuration + +## Configuration Keys + +This section details how to programmatically configure the Content Analytics extension. + +The following config settings are available. These settings can also be managed within the [Adobe Content Analytics extension](/src/pages/solution/adobe-content-analytics/index.md#configure-the-content-analytics-extension). + +| Setting | Type | Default | Description | +|---|---|---|---| +| `configId` | String | N/A | [Custom datastream for Content Analytics events](/src/pages/solution/adobe-content-analytics/index.md#datastreams) (overrides edge.configId) | +| `batchingEnabled` | Boolean | true | [Enable batching](/src/pages/solution/adobe-content-analytics/index.md#batching-settings) | +| `maxBatchSize` | Integer | 10 | [Maximum events per batch](/src/pages/solution/adobe-content-analytics/index.md#batching-settings). | +| `flushInterval` | Integer | 2000 | [Flush interval (in milliseconds)](/src/pages/solution/adobe-content-analytics/index.md#batching-settings) | +| `trackExperiences` | Boolean | true | [Enable experience tracking](/src/pages/solution/adobe-content-analytics/index.md#general-settings). | +| `excludedAssetLocationsRegexp` | String | - | [Asset location regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `excludedAssetUrlsRegexp` | String | - | [Asset URL regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `excludedExperienceLocationsRegexp` | String | - | [Experience location regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `debugLogging` | Boolean | false | [Verbose logging](/src/pages/solution/adobe-content-analytics/index.md#general-settings). | + +All keys are prepended with `contentanalytics.`. + +You can configure the extension through the Data Collection Content Analytics extension UI, or programmatically. + + + +Android + + + +iOS + + + +## Datastream + +You can stream data from content analytics through a separate datastream. + +To route Content Analytics to a different datastream: + +```json +{ + "edge.configId": "main-datastream-id", + "contentanalytics.configId": "content-analytics-datastream-id" +} +``` + +If `contentanalytics.configId` is not set, the default `edge.configId` is used. + +## Batching + +You can use the following flush triggers: + +* Batch reaches `maxBatchSize`. +* Timer reaches `batchFlushInterval` (ms). +* App backgrounds. + +```json +{ + "contentanalytics.batchingEnabled": true, + "contentanalytics.maxBatchSize": 10, + "contentanalytics.batchFlushInterval": 2000 +} +``` + +To disable flushes for immediate sends: + +```json +{ "contentanalytics.batchingEnabled": false } +``` + + + +Batching only affects network delivery. Features like asset attribution, experience tracking, and featurization work the same whether batching is enabled or disabled. + +## Filtering + +You filter content analytics events through regular expressions. + +### By URL + +An example of a regex that filters out URLs. + +```json +{ "contentanalytics.excludedAssetUrlsRegexp": ".*\\.gif$|.*spinner.*" } +``` + +### By Location + +An example of a regex that filers our asset and experience locations. + +```json +{ "contentanalytics.excludedAssetLocationsRegexp": "^(debug|test).*" } +{ "contentanalytics.excludedExperienceLocationsRegexp": "^admin\\..*" } +``` + +## Privacy + +To manage privacy, use the consent API's. + +### Edge Consent + + + +Android + + + +iOS + + + +| Value | Result | +|-------|--------| +| `"y"` | Events sent | +| `"n"` | Events dropped | +| `"p"` | Events queued | + +### Legacy + +The legacy privacy APIs also should work. + + + +Android + + + +iOS + + + +### Data Deletion + +To delete data, use `resetIdentities()` to reset identities, clear cache and queue. + + + +Android + + + +iOS + + + +## Featurization + +Featurization is configured automatically. Sends experience content to the machine learning service for feature extraction. + +See below for an example of the payload to send. + +```json +{ + "experienceId": "mobile-abc123", + "orgID": "YOUR_ORG@AdobeOrg", + "content": { + "images": [{"value": "https://...jpg", "style": {}}], + "texts": [{"value": "Title", "style": {"role": "headline"}}], + "ctas": [{"value": "Buy", "style": {"enabled": true}}] + } +} +``` + +## Tuning Batch Settings + +The default settings (`maxBatchSize: 10`, `batchFlushInterval: 2000` ms) should work well for most apps. Adjust these settings based on your event volume: + +| Events per Minute | maxBatchSize | batchFlushInterval (ms) | Notes | +|-------------------|--------------|-------------------------|-------| +| < 10 | 10 (default) | 2000 (default) | Default works well | +| 10-50 | 15-25 | 3000 | Reduces network calls | +| > 50 | 25-50 | 5000 | High-volume optimization | + +**Trade-off:** Larger batches reduce network overhead but increase latency before data appears in reporting. + +## Debugging + +Use `setLogLevel()` to set the debugging level. + + + +Android + + + +iOS + + + +Log entries are tagged. See below for the various tags. + +* `[ContentAnalytics]` - main +* `[ContentAnalytics.Batch]` - batching +* `[ContentAnalytics.Featurization]` - ML service diff --git a/src/pages/solution/adobe-content-analytics/api-reference.md b/src/pages/solution/adobe-content-analytics/api-reference.md new file mode 100644 index 0000000000..387344afd3 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/api-reference.md @@ -0,0 +1,166 @@ +--- +title: Adobe Campaign Standard API reference +description: An API reference for the Adobe Campaign Standard mobile extension. +keywords: +- Adobe Campaign Standard +- API reference +--- + +import Alerts from '/src/pages/resources/alerts.md' +import Tabs from './tabs/api-reference.md' + +# Adobe Content Analytics API reference + +This section details the publicly available API's for Content Analytics. + +## registerExperience + +Registers an experience and return an ID to track the experience. + + + +Android + + + +iOS + + + +## trackAsset + +Tracks an asset with an explicit defined interaction type. + + + +Android + + + +iOS + + + +## trackAssetClick + +Convenience method for tracking asset clicks. + + + +Android + + + +iOS + + + +## trackAssetCollection + +Tracks multiple assets with the same interaction type. + + + +Android + + + +iOS + + + +## trackAssetView + +Convenience method for tracking asset views. + + + +Android + + + +iOS + + + +## trackExperienceClick + +Tracks when an experience is clicked. + + + +You must call [`registerExperience()`](#registerexperience) before you can track experience clicks. See the Experience Tracking Guide for detailed usage patterns. + + + +Android + + + +iOS + + + +## trackExperienceView + +Tracks when an experience is viewed. + + + +You must call [`registerExperience()`](#registerexperience) before you can track experience views. See the [Experience Tracking Guide](./experience-tracking.md) for detailed usage patterns. + + + +Android + + + +iOS + + + +## Data types + +### contentItem + +Represents the content within an experience (assets, texts, CTAs). + + + +Android + + + +iOS + + + +### interactionType + +Defines the type of interaction, either a view or a click. + + + +Android + + + +iOS + + + +## Configuration + +The following config settings are available. These settings can also be managed within the [Adobe Content Analytics extension](/src/pages/solution/adobe-content-analytics/index.md#configure-the-content-analytics-extension). + +| Setting | Type | Default | Description | +|---|---|---|---| +| `configId` | String | N/A | [Custom datastream for Content Analytics events](/src/pages/solution/adobe-content-analytics/index.md#datastreams) (overrides edge.configId) | +| `batchingEnabled` | Boolean | true | [Enable batching](/src/pages/solution/adobe-content-analytics/index.md#batching-settings) | +| `maxBatchSize` | Integer | 10 | [Maximum events per batch](/src/pages/solution/adobe-content-analytics/index.md#batching-settings). | +| `flushInterval` | Integer | 2000 | [Flush interval (in milliseconds)](/src/pages/solution/adobe-content-analytics/index.md#batching-settings). | +| `trackExperiences` | Boolean | true | [Enable experience tracking](/src/pages/solution/adobe-content-analytics/index.md#general-settings). | +| `excludedAssetLocationsRegexp` | String | - | [Asset location regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `excludedAssetUrlsRegexp` | String | - | [Asset URL regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `excludedExperienceLocationsRegexp` | String | - | [Experience location regex pattern](/src/pages/solution/adobe-content-analytics/index.md#exclusions). | +| `debugLogging` | Boolean | false | [Verbose logging](/src/pages/solution/adobe-content-analytics/index.md#general-settings). | diff --git a/src/pages/solution/adobe-content-analytics/assets/index/configuration.png b/src/pages/solution/adobe-content-analytics/assets/index/configuration.png new file mode 100644 index 0000000000..d85fc4937a Binary files /dev/null and b/src/pages/solution/adobe-content-analytics/assets/index/configuration.png differ diff --git a/src/pages/solution/adobe-content-analytics/crash-recovery.md b/src/pages/solution/adobe-content-analytics/crash-recovery.md new file mode 100644 index 0000000000..424297d014 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/crash-recovery.md @@ -0,0 +1,391 @@ +--- +title: Content Analytics Crash Recovery Architecture +description: Learn how to use experience tracking in Content Analytics. +keywords: +- Adobe Analytics +- Product overview +--- + +import Tabs from './tabs/crash-recovery.md' +import InitializeSDK from '/src/pages/resources/initialize.md' + +# Crash recovery architecture + +## Overview + +Content Analytics uses `PersistentHitQueue` to protect against data loss during the batching window (0-5 seconds). Events are written to disk immediately when tracked. On next app launch, any persisted events are recovered from disk into memory for processing, then cleared from disk (no data loss - events are safely in memory before disk cleanup). + +## How It Works + +```00% hone +User tracks event + └─> Event added to memory + disk (crash-safe) + │ + └─> Batching (0-5 seconds) + │ + └─> Flush triggered + │ + ├─> Process accumulated events + ├─> Calculate aggregated metrics + └─> Dispatch to Edge Network (Edge guarantees delivery) +``` + +## Architecture Components + +### BatchCoordinator + +**Responsibilities:** + +* Manages batching logic (count threshold and time-based flush) +* Writes incoming events to disk immediately via `PersistentHitQueue` +* Maintains in-memory event counters +* Triggers flush when threshold reached (10 events or 5 seconds) +* Coordinates between `DirectHitProcessor` and `ContentAnalyticsOrchestrator` + +**Key Methods:** + + + +Android + + + +iOS + + + +### DirectHitProcessor + +**Responsibilities:** + +* Implements `HitProcessing` protocol for `PersistentHitQueue` integration +* Accumulates events in memory for fast batching +* On recovery: loads events from disk into memory, then clears disk (no data loss) + +**Event Lifecycle:** + + + +Android + + + +iOS + + + +### PersistentHitQueue (AEPServices) + +**Provides:** + +* Two separate queues: `asset.events` and `experience.events` +* SQLite-backed persistence (survives crashes, force-quit, background termination) +* Automatic processing via `beginProcessing()` +* Thread-safe operations + +**Storage:** + +* Events encoded as JSON via `Event: Codable` +* Each event wrapped with type metadata (`asset` or `experience`) +* Unique identifier: `event.id.uuidString` + +## Detailed Timeline Example + +```text +Time │ Event │ Memory │ Disk │ Safe? +───────┼──────────────────────────────────────┼────────┼──────┼─────── +00.00s │ User views Asset A │ ✓ │ ✓ │ ✅ YES +00.01s │ Event written to disk │ ✓ │ ✓ │ ✅ YES +00.50s │ User clicks Asset B │ ✓ │ ✓ │ ✅ YES +01.00s │ User clicks Asset B │ ✓ │ ✓ │ ✅ YES + │ [Batching window - events on disk] │ │ │ +02.00s │ Timer fires → Flush triggered │ ✓ │ ✓ │ ✅ YES +02.01s │ Process accumulated events │ ✓ │ ✓ │ ✅ YES +02.02s │ Calculate metrics (1 view, 2 clicks) │ ✓ │ ✓ │ ✅ YES +02.03s │ Dispatch to Edge Network │ ✗ │ ✗ │ ✅ YES* + │ (*Edge guarantees delivery) │ │ │ + +Legend: +✓ = Present +✗ = Not present +``` + +Events stay on disk during the entire batching window. Once events are handed off to Edge, their persistence takes over. + +## Crash Scenarios + +### Scenario 1: Crash During Batching (0-5s window) + +```text +Status: Events in memory + disk +Crash: ⚡ App terminated + └─> Memory lost ✗ + └─> Disk persists ✓ + +Recovery on Next Launch: +1. PersistentHitQueue.beginProcessing() starts +2. DirectHitProcessor.processHit() called for each persisted event +3. Events accumulated in memory, cleared from disk +4. Normal batch processing resumes + +Result: ✅ ZERO DATA LOSS +``` + +### Scenario 2: Crash During Flush + +```text +Status: Events being processed +Crash: ⚡ App terminated mid-dispatch + └─> Memory lost ✗ + └─> Events may still be on disk if not yet processed + +Recovery on Next Launch: +1. Any remaining events on disk are recovered +2. Re-accumulated and dispatched on next flush + +Result: ✅ ZERO DATA LOSS (possible duplicate if crash after Edge dispatch) +``` + +### Scenario 3: Crash After Edge Dispatch + +```text +Status: Events dispatched to Edge +Crash: ⚡ App terminated + └─> Disk already cleared during processHit() + └─> Edge has the events + +Result: ✅ ZERO DATA LOSS - Edge guarantees delivery +``` + +## Edge Network Handoff + +Once events are dispatched to Edge extension: + +```text +ContentAnalytics → runtime.dispatch(event) → Event Hub → Edge Extension + └─> Edge.PersistentHitQueue + └─> Network retries + └─> Exponential backoff +``` + +**Handoff Point:** After `eventDispatcher.dispatch()` completes, Edge extension owns persistence. + +**Edge Guarantees:** Once Edge receives the event, it handles persistence, retries, and delivery confirmation. + +## Metrics Calculation + +Metrics are **derived from events**, not stored separately: + + + +Android + + + +iOS + + + +This avoids state sync issues. Just events are counted on flush. If the app crashes, the restored events give the same metrics. + +## Configuration + +```json +{ + "contentanalytics.batchingEnabled": true, + "contentanalytics.maxBatchSize": 10, + "contentanalytics.batchFlushInterval": 2000 +} +``` + +**Parameters:** + +* `maxBatchSize`: Event count threshold (default: 10) +* `batchFlushInterval`: Timer interval for periodic flush in milliseconds (default: 2000 ms = 2s). Max wait time is derived from this (2.5× = 5000 ms). +* `batchingEnabled`: Set to `false` for immediate dispatch (no batching) + +## Performance Characteristics + +| Operation | Time | Notes | +|-----------|------|-------| +| Event persistence | ~1-2ms | SQLite write | +| Event recovery | ~5-10ms | SQLite read on launch | +| Batch flush | ~10-20ms | Metrics calculation + Edge dispatch | +| Memory per event | ~2KB | Event object + metadata | +| Disk per event | ~1-2KB | JSON encoding | + +**Memory Usage:** With default batch size (10), worst-case memory is ~20-40KB (negligible). + +**Network Efficiency:** Batching reduces Edge Network calls by 10x for high-volume tracking. + +## Thread Safety + +All operations use Kotlin coroutines with `Mutex` for thread-safe access: + + + +Android + + + +## Testing Crash Recovery + +### Test 1: Crash During Batching + +1. Track 5 asset events +2. DO NOT wait for flush timer +3. Force-quit app (⌘+Q or kill process) +4. Relaunch app +5. Track 5 more asset events +6. Wait 2 seconds for flush +7. Verify: 1 Edge event with 10 aggregated interactions + +### Test 2: Crash During Flush + +1. Track 10 asset events (triggers immediate flush) +2. Set breakpoint in `sendToEdge()` +3. Force-quit app at breakpoint +4. Relaunch app +5. Wait 5 seconds +6. Verify: Events re-dispatched (possible duplicate) + +### Test 3: Background Termination + +1. Track events +2. Background app +3. OS terminates app (memory pressure) +4. Relaunch app +5. Verify: Events recovered and dispatched + +## Implementation Details + +### Key Files + +* `BatchCoordinator.swift` - Batching logic and persistence coordination +* `DirectHitProcessor.swift` - Crash recovery and event accumulation +* `ContentAnalyticsOrchestrator.swift` - Metrics calculation and Edge dispatch +* `PersistentHitQueue` (AEPServices) - SQLite-backed queue + +### Thread Safety + +* All operations use serial dispatch queues +* `batchQueue` (BatchCoordinator) - batch operations +* `queue` (DirectHitProcessor) - hit processing + +### Data Flow + +```text +Event tracked + └─> BatchCoordinator.addAssetEvent() + ├─> DirectHitProcessor.accumulateEvent() [memory] + ├─> PersistentHitQueue.queue() [disk] + └─> checkAndFlushIfNeeded() + └─> performFlush() + └─> DirectHitProcessor.processAccumulatedEvents() + └─> Orchestrator.processAssetEvents() + └─> EventDispatcher.dispatch() [→ Edge] +``` + +### Callback Chain Architecture + +The SDK uses a callback chain to decouple components while maintaining type safety: + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ INITIALIZATION PHASE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ContentAnalyticsFactory.createOrchestrator() │ +│ │ │ +│ ├─> Creates BatchCoordinator(assetQueue, experienceQueue, state) │ +│ │ └─> DirectHitProcessor initialized with no-op callbacks │ +│ │ │ +│ ├─> Creates ContentAnalyticsOrchestrator(batchCoordinator, ...) │ +│ │ │ +│ └─> Wires callbacks: batchCoordinator.setCallbacks( │ +│ assetCallback: orchestrator.processAssetEvents, │ +│ experienceCallback: orchestrator.processExperienceEvents │ +│ ) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ RUNTIME DATA FLOW │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ User calls ContentAnalytics.trackAssetInteraction() │ +│ │ │ +│ v │ +│ ┌──────────────────┐ │ +│ │ BatchCoordinator │ │ +│ │ addAssetEvent() │──────────────────────────────────────────┐ │ +│ └────────┬─────────┘ │ │ +│ │ │ │ +│ v v │ +│ ┌────────────────────┐ ┌─────────────────────┐ │ +│ │ DirectHitProcessor │ │ PersistentHitQueue │ │ +│ │ accumulateEvent() │ │ queue() [disk] │ │ +│ │ [memory buffer] │ └─────────────────────┘ │ +│ └────────┬───────────┘ │ +│ │ │ +│ │ (on flush trigger: count >= 10 or timer >= 2s) │ +│ v │ +│ ┌────────────────────────────┐ │ +│ │ DirectHitProcessor │ │ +│ │ processAccumulatedEvents() │ │ +│ └────────┬───────────────────┘ │ +│ │ │ +│ │ invokes processingCallback([events]) │ +│ v │ +│ ┌─────────────────────────────────┐ │ +│ │ ContentAnalyticsOrchestrator │ │ +│ │ processAssetEvents([events]) │ │ +│ │ ├─> Group by asset key │ │ +│ │ ├─> Calculate metrics │ │ +│ │ └─> Build XDM payload │ │ +│ └────────┬────────────────────────┘ │ +│ │ │ +│ v │ +│ ┌───────────────────┐ │ +│ │ EdgeEventDispatcher│ │ +│ │ dispatch() │──────────────> Edge Network │ +│ └───────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +Callbacks avoid circular dependencies - BatchCoordinator doesn't need to import Orchestrator. Also this makes testing easier since you can inject mocks. + +### Logging + +Enable verbose logging to debug crash recovery: + +```swift +Log.setLogLevel(.trace) +``` + +Look for: + +```text +[BATCH_PROCESSOR] Accumulated ASSET event | ID: +[BATCH_PROCESSOR] Recovered event from disk | Type: asset | ID: +[BATCH_PROCESSOR] Processing 5 asset events +``` + +## Comparison with Edge Extension + +| Feature | Content Analytics | Edge Extension | +|---------|------------------|----------------| +| Pre-dispatch persistence | ✅ YES (0-5s) | ❌ NO | +| Batching | ✅ YES | ❌ NO | +| Post-dispatch persistence | ✅ Edge's queue | ✅ PersistentHitQueue | +| Network retries | ✅ Edge handles | ✅ Exponential backoff | +| Crash recovery during batch | ✅ FULL | N/A | + +Content Analytics batches events for 0-5 seconds before dispatch. Without disk persistence during that window, crashes would lose data. Edge dispatches immediately so it doesn't need this. + +## Known Limitations + +1. **No dispatch confirmation:** Extensions cannot receive callbacks from Edge to confirm receipt +2. **Possible duplicates:** Crash during Edge dispatch may cause duplicate events (Edge deduplication handles this) +3. **Memory overhead:** Events held in memory + disk during batching (minimal: ~40KB) diff --git a/src/pages/solution/adobe-content-analytics/experience-tracking.md b/src/pages/solution/adobe-content-analytics/experience-tracking.md new file mode 100644 index 0000000000..8332232739 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/experience-tracking.md @@ -0,0 +1,685 @@ +--- +title: Experience Tracking Usage Guide +description: Learn how to use experience tracking in Content Analytics. +keywords: +- Adobe Analytics +- Product overview +--- +import Tabs from './tabs/experience-tracking.md' +import InitializeSDK from '/src/pages/resources/initialize.md' + +# Experience tracking + +Experience tracking measures how users interact with complete experiences (combinations of images, text, and CTAs) in your app. + +## Quick start + +You first register the experience. Then you can track the view of the experience, when the experience becomes visible. Or you can track the interaction on the experience, when the experience gets clicked (tapped). + + + +Android + + + +iOS + + + +## Registration required + +You must register an experience definition before tracking views or clicks. If you don't: + +* Asset attribution won't work. +* Featurization hits won't be sent. +* A warning will be logged. + +### Basic usage + +Basis usage of experience tracking is that you first register the experience, and then track the experience view or click. + +### Register the experience + +Register the experience once with all of its content. + + + +Android + + + +iOS + + + +### Track interactions + +Then track the experience. + + + +Android + + + +iOS + + + +## Session lifecycle + +Experience definitions are cached in memory for the duration of the app session. After app restart or crash, you'll need to re-register experiences before tracking. + + + +Android + + + +iOS + + + +Re-registration is idempotent. Calling `registerExperience()` with the same content returns the same ID with no negative side effects. The featurization service is also idempotent, so even if the same experience definition is sent multiple times (for example, after cache eviction or app restart), there's no duplication or data inconsistency on the backend. + +### Cache Behavior + +The SDK uses an LRU (Least Recently Used) cache with a capacity of 100 experience definitions: + +* Capacity: 100 definitions maximum. +* Eviction: When full, least recently used definitions are removed. +* Memory-only: Not persisted to disk. + +The benefits are: + +* Fast lookups for asset attribution. +* Bounded memory usage (~20-40KB worst case). +* Automatic cleanup of stale definitions. +* No disk I/O overhead. +* Safe re-registration: Featurization service handles duplicates gracefully. + +For most apps, 100 definitions is sufficient. If you're registering more unique experiences per session, consider reusing experience IDs where content is identical (same content = same ID). + +## Implementation patterns + +See below for examples of implementation patterns. + +### Single screen + +Implementation of experience tracking for a single screen. + + + +Android + + + +iOS + + + +### Collection or feed + +Implementation of experience tracking for a collection or a feed. + + + +Android + + + +iOS + + + +### Experience ID generation + +Experience IDs are deterministic - the same content always produces the same ID. The algorithm: + +1. Sort text values alphabetically. +1. Sort asset URLs alphabetically. +1. Sort CTA values alphabetically. +1. Join all with `|` separator (texts, then assets, then CTAs). +1. SHA-1 hash the combined string. +1. Take first 12 hex characters. +1. Prefix with `mobile-`. + +#### Example + +Imagine the following details: + +```text +// Content: texts=["$99", "Product"], assets=["img.jpg"], ctas=["Buy"] +// Sorted & joined: "Product|$99|img.jpg|Buy" +// SHA-1 → first 12 chars → "mobile-a1b2c3d4e5f6" +``` + +This means you can: + +* Pre-compute IDs server-side for consistent cross-platform IDs. +* Cache by content hash instead of arbitrary keys. +* Detect content changes by comparing IDs. + + + +Android + + + +iOS + + + +## Missing registration warning + +If you track without registering, you will see this warning. + + + +Experience definition not found for 'exp-123'. Call `registerExperience()` before tracking views/clicks. + +This means: + +* View/click events still go to Analytics. +* But asset attribution won't work. +* Featurization service won't get the data. + +Fix the warning by registering the experience first. + + + +Android + + + +iOS + + + +## Asset attribution + +When you register an experience with assets, the SDK links those asset URLs to the experience. This enables asset attribution: connecting standalone asset tracking events to their parent experience. + + + +Asset attribution works regardless of the `batchingEnabled` setting. The SDK caches experience definitions locally, so attribution is based on the registration cache - not on how events are batched for network delivery. + +### How it works + +See below how asset attribution works. + + + +Android + + + +iOS + + + +When the analytics backend receives `trackAssetView` for `hero.jpg`, the backend attributes that view to the `Summer Sale` experience because the asset URL was registered. + +### Without attribution + +You can track an asset without registering the experience first. + + + +Android + + + +iOS + + + +The asset view is still recorded, but the asset view is not linked to any experience. As a result, you lose: + +* Which experience contained this asset. +* Performance metrics per experience. +* A/B test attribution. + +## Location strategy + +The `experienceLocation` and `assetLocation` parameters control how metrics are grouped in Customer Journey Analytics. + +### With location - Metrics per placement + +Track the same experience at different locations. + + + +Android + + + +iOS + + + +A sample Customer Journey Analytics report for this scenario will look like: + +| Experience | Location | Views | Clicks | CTR | +|---|---|--:|--:|--:| +| Summer Sale | `homepage.`
`hero` | 10,000 | 500 | 5% | +| Summer Sale | `product.`
`sidebar` | 3,000 | 90 | 3% | +| Summer Sale | `checkout.`
`upsell` | 1,000 | 150 | 15% | + +You can use this report to answer questions like *"Where does this experience perform best?"* + +### Without location - global metrics + +Track experiences without location details to get aggregate metrics. + + + +Android + + + +iOS + + + +A sample Customer Journey Analytics report for this scenario will look like: + +| Experience | Views | Clicks | CTR | +|---|---|--:|--:| +| Summer Sale | 14,000 | 740 | 5.3% | + +You can use this report to answer questions like *How is this experience performing overall?* + +### Same asset - different locations + +Track the same asset on different locations. + + + +Android + + + +iOS + + + +Customer Journey Analytics report will look like: + +| Experience | Location | Views | Clicks | +|---|---|--:|--:| +| Summer Sale | `homepage` | 50,000 | 2,500 | +| Summer Sale | `category.`
`electronics` | 8,000 | 320 | +| Summer Sale | `search.`
`results` | 3,000 | 45 | + +## Location naming convention + +Use a consistent location naming hierarchy to filter on locations easily in Customer Journey Analytics. + +```text +screen.section.subsection +``` + +Examples are: + +* `homepage.hero` +* `homepage.featured` +* `product.detail.recommendations` +* `cart.upsell` +* `search.results.sponsored` + +### When to use a location + +See the following goals and whether you should consider to use locations or not. + +| Goal | Location | +|---|---| +| Compare same content across placements | ✅ Set location | +| A/B test content in a specific spot | ✅ Set location| +| Track overall content performance | ❌ Omit location | +| Simple asset tracking (no placement analysis) | ❌ Omit location | + +## Machine learning powered analytics + +When you register experiences, the featurization service analyzes the content and extracts machine learning (ML) attributes like persuasion strategy, emotional tone, content category, and more. These attributes are then available in Customer Journey Analytics for advanced analysis. + +### Performance by persuasion strategy + +After featurization, Customer Journey Analytics can show which persuasion strategies work best in each location. + +Sample Customer Journey Analytics report - persuasion strategy by location: + +| Location | Persuasion strategy | Views | Click | CTR | +|---|---|--:|--:|--:| +| `homepage.`
`hero` | Urgency | 10,000 | 800 | 8% | +| `homepage.`
`hero` | Social Proof | 10,000 | 650 | 6.5% | +| `homepage.`
`hero` | Scarcity | 10,000 | 720 | 7.2% | +| `checkout.`
`upsell` | Urgency | 2,000 | 300 | 15% | +| `checkout.`
`upsell` | Social Proof | 2,000 | 180 | 9% | + +Insight: **Urgency** messaging performs best at checkout (+15% CTR), while **Social Proof** works better on homepage. + +### Performance by content category + +Sample Customer Journey Analytics report - asset category performance: + +| Asset Category | Location | Views | Engagement | +|---|---|--:|--:| +| Lifestyle | `homepage` | 50,000 | 12% | +| Product-focused | `homepage` | 50,000 | 8% | +| Lifestyle | `product.detail` | 20,000 | 6% | +| Product-focused | `product.detail` | 20,000 | 14% | + +Insight: **Lifestyle** imagery works on homepage, but **Product-focused** images convert better on detail pages. + +### How it works + +1. You track: `registerExperience()` sends content to the featurization service. +1. ML analyzes: service extracts persuasion strategy, tone, category, and more. +1. Attributes are stored: machine learning attributes are linked to the experience/asset. +1. Customer Journey Analytics queries: reports can segment by any machine learning attribute and location. + + + +Android + + + +iOS + + + +In Customer Journey Analytics, you can then filter or group by persuasion strategy to see what messaging resonates in each location. + +## Custom metrics with `additionalData` + +The `additionalData` parameter lets you attach custom metrics to tracking events. These custom metrics appear in Customer Journey Analytics as additional dimensions or metrics. + +### Asset performance metrics + +To get asset performance metrics, see this example. + + + +Android + + + +iOS + + + +### Asset view duration + +To get asset view duration, see this example. + + + +Android + + + +iOS + + + +### Experience engagement metrics + +To get experience engagement metrics, see this example. + + + +Android + + + +iOS + + + +### Common custom metrics + +| Metric | Type | Description | +|---|---|---| +| `assetLoadTime` | Double | Image/video load time (ms).. | +| `assetViewDuration` | Double | Time asset was visible (ms). | +| `assetSize` | Int | Asset file size (bytes). | +| `experienceViewDuration` | Double | Time before interaction (ms). | +| `scrollDepth` | Double | Scroll position when viewed (%). | +| `viewportPosition` | String | `above_fold` / `below_fold`. | +| `interactionIndex` | Int | Nth click on this session .| +| `experimentVariant` | String | A/B test variant ID.| +| `deviceOrientation` | String | `portrait` / `landscape`. | + +#### Customer Journey Analytics report with custom metric + +For example, reporting on average load time by asset location. + +| Location | Avg Load Time | Avg View Duration | +|---|--:|--:| +| `homepage.hero` | 120ms | 3.2s | +| `product.gallery` | 85ms | 8.5s | +| `search.results` | 45ms | 1.1s | + +Insight: Gallery images load **slower** but get **eight times more viewing time.** + +## Debugging with Assurance + +Adobe Assurance (Project Griffon) lets you inspect tracking events in real-time. Connect your app to an Assurance session to see exactly what payloads are being sent. + +### Setup + +To setup Assurance, import the extension and start the session. + + + +Android + + + +iOS + + + +### What You'll See in Assurance + +1. Track Asset Events + + When you call `trackAssetView()` or `trackAssetClick()`, you'll see: + + ```json + Event: Track Asset + Type: com.adobe.eventType.contentAnalytics + Source: com.adobe.eventSource.requestContent + + Payload: + { + "assetURL": "https://example.com/hero.jpg", + "interactionType": "view", + "assetLocation": "homepage.hero", + "assetExtras": { + "assetLoadTime": 120, + "assetSize": 45000 + } + } + ``` + +2. Track Experience Events + + * When you call `registerExperience()`: + + ```json + Event: Track Experience + Type: com.adobe.eventType.contentAnalytics + + Payload: + { + "experienceId": "mobile-abc123...", + "interactionType": "definition", + "assetURLs": ["https://example.com/hero.jpg"], + "texts": [ + {"value": "Summer Sale", "styles": {"role": "headline"}} + ], + "ctas": [ + {"value": "Shop Now", "styles": {"enabled": true}} + ] + } + ``` + + * When you call `trackExperienceView()` or `trackExperienceClick()`: + + ```json + Event: Track Experience + Type: com.adobe.eventType.contentAnalytics + + Payload: + { + "experienceId": "mobile-abc123...", + "interactionType": "view", + "experienceLocation": "homepage.hero", + "experienceExtras": { + "experienceViewDuration": 3500 + } + } + ``` + +3. Edge Network Events + + After batching, you'll see the Edge request: + + ```json + Event: Edge Request + Type: com.adobe.eventType.edge + + Payload: + { + "xdm": { + "eventType": "contentanalytics.asset.view", + "_contentanalytics": { + "asset": { + "url": "https://example.com/hero.jpg", + "location": "homepage.hero" + } + } + } + } + ``` + +### Debugging checklist + +| What to Check | Where in Assurance | +|---|---| +| Event dispatched | Look for Track Asset / Track Experience events. | +| Correct payload | Expand event → check assetURL, experienceId, etc. | +| Batching working | Multiple events → single Edge request. | +| Edge delivery | Look for Edge Request after batch flush. | +| Consent status | Check Edge Consent events. | + +### Common Issues in Assurance + +No events appearing: + +* Check extension is registered. +* Verify MobileCore.dispatch() is being called. + +Events but no Edge request: + +* Check consent status (must be "yes" or "pending"). +* Wait for batch timeout (default 5s) or threshold (default 10 events). + +Missing experienceId in track events: + +* Ensure registerExperience() was called first +* Check the returned ID is being passed to track methods + +## Testing + +To test your implementation, enable verbose logging. + + + +Android + + + +iOS + + + +Then look for registration confirmation + +```text +[ContentAnalytics] Stored experience definition: exp-abc123 with 3 assets +``` + +And tracking confirmation: + +```text +[ContentAnalytics] Experience event processed successfully: track-view - exp-abc123 +Test cross-session: register, force quit, relaunch, track same ID. No warning should appear. +``` + +## Troubleshooting + +* **Experience definition not found** warning: Register the experience before tracking it. +* Assets not attributed. Same issue - register with `assetURLs` before tracking. +* Duplicate registrations: Check if already registered before calling `registerExperience()`: + + + +Android + + + +iOS + + + +* Or compute the ID yourself using the algorithm above for content-based caching. + +## Common patterns + +Common implementation patterns are illustrated below. + +### Carousel/Banner + +To implement a carousel or banner, see below for an example. + + + +Android + + + +iOS + + + +### Product Grid + +To implement a product grid, see below for an example. + + + +Android + + + +iOS + + + +### Reusable Tracking Component + +To implement a reusable tracking component, see below for an example. + + + +Android + + + +iOS + + +``` diff --git a/src/pages/solution/adobe-content-analytics/index.md b/src/pages/solution/adobe-content-analytics/index.md new file mode 100644 index 0000000000..1f72791e76 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/index.md @@ -0,0 +1,81 @@ +--- +title: Adobe Content Analytics extension +description: Configuring the Adobe Content Analytics extension in the Data Collection UI +keywords: +- Adobe Analytics +- Product overview +--- +import Tabs from './tabs/index.md' +import InitializeSDK from '/src/pages/resources/initialize.md' + +# Adobe Content Analytics + +## Configure Content Analytics extension in the Data Collection UI + +1. In the Data Collection UI, select the **Extensions** tab. +1. On the **Catalog** tab, locate the **Adobe Content Analytics** extension, and select **Install**. +1. Configure the extension settings. For more information, see [Configure the Content Analytics extension](#configure-the-content-analytics-extension). +1. Select **Save**. +1. Follow the publishing process to update your SDK configuration. + +## Configure the Content Analytics extension + +To configure the Content Analytics extension, complete the following steps: + +![Content Analytics Extension Configuration](./assets/index/configuration.png) + +### Sandbox + +Select a **Sandbox** to use for Content Analytics. + +### Datastreams + +Select the **Datastream** to use for Content Analytics for the **Production** (required), **Staging**, and **Development** environment. + +### General Settings + +Enable or disable **Track Experiences** to track experiences in Content Analytics or not. Default is enabled (true). + +Select **Enable Debug Logging** to enable verbose debug logging for Content Analytics. Default is disabled (false). + +### Batching Settings + +Select **Enable Batching** to enable batching for Content Analytics. + +Enter a value in **Max Batch Size** to define the maximum batch size. Default is `10`. + +Enter a value in **Batch Flush Interval (ms)** to define a time in miliseconds to wait before flusing batched events. Default is `2000` (2 seconds). + +### Exclusions + +Specify exclusions for asset URLs, assets locations, and experience locations. + +* Enter an **Asset URL Pattern** to specify a regular expression to filter which asset URLs should be excluded when collecting data for Content Analytics. For example: `.*\\.gif$|.*\\.svg$` to exclude GIF or SVG files.
Use **Test Regex** to open the **Regular Expression Tester** where you can validate your regular expression. +* Enter an **Asset Location Pattern** to specify a regular expression to filter which asset locations should be excluded when collecting data for Content Analytics. For example: `^(debug|test).*` to exclude asset location that contain `debug` or `test`.
Use **Test Regex** to open the **Regular Expression Tester** where you can validate your regular expression. +* Enter an **Experience Location Pattern** to specify a regular expression to filter which experience locations should be excluded when collecting data for Content Analytics. For example: `^test\\..*|^dev\\..*` to exclude any experience location that contains `test.` or `dev.`
Use **Test Regex** to open the **Regular Expression Tester** where you can validate your regular expression. + +## Add Content Analytics extension to your app + +To add the Content Analytics extension to your app, follow the steps below based on the platform and package manager you use. + +### Include Content Analytics extension as an app depencency. + +Add MobileCore, Edge, EdgeIdentity, and Content Analytics as dependencies to your project. + + + +Kotlin
(Android) + + + +Groovy
(Android) + + + +SPM
(iOS) + + + +CocoaPods
(iOS) + + diff --git a/src/pages/solution/adobe-content-analytics/tabs/advanced-configuration.md b/src/pages/solution/adobe-content-analytics/tabs/advanced-configuration.md new file mode 100644 index 0000000000..5cae02183b --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/tabs/advanced-configuration.md @@ -0,0 +1,89 @@ +--- +noIndex: true +--- + +import Alerts from '/src/pages/resources/alerts.md' + + + +```java +MobileCore.updateConfiguration(mapOf( + "contentanalytics.maxBatchSize" to 20, + "contentanalytics.batchFlushInterval" to 5000 +)) +``` + + + +```swift +MobileCore.updateConfigurationWith(configDict: [ + "contentanalytics.maxBatchSize": 20, + "contentanalytics.batchFlushInterval": 5000 +]) +``` + + + +```java +// Opt in +Consent.update(mapOf("consents" to mapOf("collect" to mapOf("val" to "y")))) + +// Opt out +Consent.update(mapOf("consents" to mapOf("collect" to mapOf("val" to "n")))) + +// Pending +Consent.update(mapOf("consents" to mapOf("collect" to mapOf("val" to "p")))) +``` + + + +```swift +// Opt in +Consent.update(with: ["consents": ["collect": ["val": "y"]]]) + +// Opt out +Consent.update(with: ["consents": ["collect": ["val": "n"]]]) + +// Pending +Consent.update(with: ["consents": ["collect": ["val": "p"]]]) +``` + + + +```java +MobileCore.setPrivacyStatus(MobilePrivacyStatus.OPT_IN) // send +MobileCore.setPrivacyStatus(MobilePrivacyStatus.OPT_OUT) // drop + clear +MobileCore.setPrivacyStatus(MobilePrivacyStatus.UNKNOWN) // queue +``` + + + +```swift +MobileCore.setPrivacyStatus(.optedIn) // send +MobileCore.setPrivacyStatus(.optedOut) // drop + clear +MobileCore.setPrivacyStatus(.unknown) // queue +``` + + + +```java +MobileCore.resetIdentities() // clears cache + queue +``` + + + +```swift +MobileCore.resetIdentities() // clears cache + queue +``` + + + +```java +MobileCore.setLogLevel(LoggingMode.VERBOSE) +``` + + + +```swift +MobileCore.setLogLevel(.debug) +``` diff --git a/src/pages/solution/adobe-content-analytics/tabs/api-reference.md b/src/pages/solution/adobe-content-analytics/tabs/api-reference.md new file mode 100644 index 0000000000..5f545a8a38 --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/tabs/api-reference.md @@ -0,0 +1,916 @@ +--- +noIndex: true +--- + + + +#### Java + +**Syntax** + +```java +static void trackAsset(String assetURL) +static void trackAsset(String assetURL, InteractionType interactionType) +static void trackAsset(String assetURL, InteractionType interactionType, String assetLocation) +static void trackAsset(String assetURL, InteractionType interactionType, String assetLocation, Map additionalData) + +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAsset( + "https://example.com/image.jpg", + InteractionType.VIEW, + "home" +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackAsset( + assetURL: String, + interactionType: InteractionType = InteractionType.VIEW, + assetLocation: String? = null, + additionalData: Map? = null +): Unit +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAsset( + assetURL: "https://example.com/image.jpg", + interactionType: InteractionType.VIEW, + assetLocation: "home" +); +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackAsset( + assetURL: String, + interactionType: InteractionType = .view, + assetLocation: String? = nil, + additionalData: [String: Any]? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackAsset( + assetURL: "https://example.com/image.jpg", + interactionType: InteractionType.VIEW, + assetLocation: "home" +); +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackAsset:(NSString *)assetURL + interactionType:(AEPInteractionType)interactionType + assetLocation:(nullable NSString *)assetLocation + additionalData:(nullable NSDictionary *)additionalData; +``` + +**Example** + +```objc +[AEPContentAnalytics trackAsset:@"https://example.com/image.jpg" + interactionType:AEPInteractionTypeView + assetLocation:@"home"]; +``` + + + +#### Java + +**Syntax** + +```java +static void trackAssetView(String assetURL) +static void trackAssetView(String assetURL, String assetLocation) +static void trackAssetView(String assetURL, String assetLocation, Map additionalData)additionalData) + +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAssetView( + "https://example.com/image.jpg", + "home" +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackAssetView( + assetURL: String, + assetLocation: String? = null, + additionalData: Map? = null +): Unit +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAssetView( + assetURL: "https://example.com/image.jpg", + assetLocation: "home" +); +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackAssetView( + assetURL: String, + assetLocation: String? = nil, + additionalData: [String: Any]? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackAsset( + assetURL: "https://example.com/image.jpg", + assetLocation: "home" +); +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackAssetView:(NSString *)assetURL + assetLocation:(nullable NSString *)assetLocation + additionalData:(nullable NSDictionary *)additionalData; + +``` + +**Example** + +```objc +[AEPContentAnalytics trackAssetView:@"https://example.com/image.jpg" + assetLocation:@"home"]; +``` + + + +#### Java + +**Syntax** + +```java +static void trackAssetClick(String assetURL) +static void trackAssetClick(String assetURL, String assetLocation) +static void trackAssetClick(String assetURL, String assetLocation, Map additionalData) +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAssetClick( + "https://example.com/image.jpg", + "home", + null +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackAssetClick( + assetURL: String, + assetLocation: String? = null, + additionalData: Map? = null +): Unit +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackAssetClick( + "https://example.com/image.jpg", + "home", + null +); +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackAssetClick( + assetURL: String, + assetLocation: String? = nil, + additionalData: [String: Any]? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackAssetClick( + assetURL: "https://example.com/image.jpg", + assetLocation: "home" +); +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackAssetClick:(NSString *)assetURL + assetLocation:(nullable NSString *)assetLocation + additionalData:(nullable NSDictionary *)additionalData; +``` + +**Example** + +```objc +[AEPContentAnalytics trackAssetClick:@"https://example.com/image.jpg" + assetLocation:@"home"]; +``` + + + +#### Java + +**Syntax** + +```java +static void trackAssetCollection(List assetURLs) +static void trackAssetCollection(List assetURLs, InteractionType interactionType) +static void trackAssetCollection(List assetURLs, InteractionType interactionType, String assetLocation) + +``` + +**Example** + +```java +ContentAnalytics.trackAssetCollection( + List.of( + "https://example.com/img1.jpg", + "https://example.com/img2.jpg" + ), + "product-carousel" +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackAssetCollection( + assetURLs: List, + interactionType: InteractionType = InteractionType.VIEW, + assetLocation: String? = null +): Unit +``` + +**Example** + +```java +ContentAnalytics.trackAssetCollection( + assetURLs = listOf( + "https://example.com/img1.jpg", + "https://example.com/img2.jpg" + ), + assetLocation = "product-carousel" +); +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackAssetCollection( + assetURLs: [String], + interactionType: InteractionType = .view, + assetLocation: String? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackAssetCollection( + assetURLs: ["https://example.com/image1.jpg", "https://example.com/image1.jpg"], + assetLocation: "home" +); +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackAssetCollectionWithAssetURLs:(NSArray *)assetURLs + interactionType:(AEPInteractionType)interactionType + assetLocation:(nullable NSString *)assetLocation; +``` + +**Example** + +```objc +[AEPContentAnalytics trackAssetCollectionWithAssetURLs:@[@"https://example.com/image1.jpg",@"https://example.com/image1.jpg"] + interactionType:AEPInteractionTypeView + assetLocation:@"home"]; +``` + + + +#### Java + +**Syntax** + +```java +static String registerExperience(List assets, List texts) +static String registerExperience(List assets, List texts, List ctas) + +``` + +**Example** + +```java +String expId = ContentAnalytics.registerExperience( + List.of( + new ContentItem("https://example.com/product.jpg") + ), + List.of( + new ContentItem("iPhone 16 Pro", Map.of("role", "headline")), + new ContentItem("$999", Map.of("role", "price")) + ), + List.of( + new ContentItem("Buy Now", Map.of("enabled", true)) + ) +); + +ContentAnalytics.trackExperienceView(expId, "product.detail") +``` + +#### Kotlin + +**Syntax** + +```java +fun registerExperience( + assets: List, + texts: List, + ctas: List? = null +): String + +``` + +**Example** + +```java +// Using InteractionType enum directly +val expId = ContentAnalytics.registerExperience( + assets = listOf( + ContentItem("https://example.com/product.jpg") + ), + texts = listOf( + ContentItem("iPhone 16 Pro", mapOf("role" to "headline")), + ContentItem("$999", mapOf("role" to "price")) + ), + ctas = listOf( + ContentItem("Buy Now", mapOf("enabled" to true)) + ) +); + +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "product.detail") +``` + + + +#### Swift + +**Syntax** + +```swift +@discardableResult +static func registerExperience( + assets: [ContentItem], + texts: [ContentItem], + ctas: [ContentItem]? = nil +) -> String +``` + +**Example** + +```swift +let expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: "https://example.com/product.jpg", styles: [:])], + texts: [ + ContentItem(value: "iPhone 16 Pro", styles: ["role": "headline"]), + ContentItem(value: "$999", styles: ["role": "price"]) + ], + ctas: [ContentItem(value: "Buy Now", styles: ["enabled": true])] +) +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "product.detail") +``` + +#### Objective-C + +**Syntax** + +```objc ++ (NSString *)registerExperienceWithAssets:(NSArray *)assets + texts:(NSArray *)texts + ctas:(nullable NSArray *)ctas; +``` + +**Example** + +```objc +NSString *expId = [AEPContentAnalytics registerExperienceWithAssets:@[ + [[AEPContentItem alloc] initWithValue:@"https://example.com/product.jpg" styles:@{}] +] +texts:@[ + [[AEPContentItem alloc] initWithValue:@"iPhone 16 Pro" styles:@{@"role": @"headline"}], + [[AEPContentItem alloc] initWithValue:@"$999" styles:@{@"role": @"price"}] +] +ctas:@[ + [[AEPContentItem alloc] initWithValue:@"Buy Now" styles:@{@"enabled": @YES}] +]]; + +[AEPContentAnalytics trackExperienceViewWithExperienceId:expId + experienceLocation:@"product.detail"]; +``` + + + +#### Java + +**Syntax** + +```java +static void trackExperienceView(String experienceId) +static void trackExperienceView(String experienceId, String experienceLocation) +static void trackExperienceView(String experienceId, String experienceLocation, Map additionalData) + +``` + +**Example** + +```java +ContentAnalytics.trackExperienceView( + expId, + "homepage.hero", + Map.of("viewDuration", 5.2) +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackExperienceView( + experienceId: String, + experienceLocation: String? = null, + additionalData: Map? = null +): Unit +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackExperienceView( + experienceId: expId, + experienceLocation: "homepage.hero", + additionalData: ["viewDuration": 5.2] +) +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackExperienceView( + experienceId: String, + experienceLocation: String? = nil, + additionalData: [String: Any]? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackExperienceView( + experienceId: expId, + experienceLocation: "homepage.hero", + additionalData: ["viewDuration": 5.2] +) +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackExperienceView:(NSString *)assetURL + assetLocation:(nullable NSString *)assetLocation + additionalData:(nullable NSDictionary *)additionalData; + +``` + +**Example** + +```objc +[AEPContentAnalytics trackExperienceView:expId + experienceLocation:@"homepage.hero" + additionalData:@{@"viewDuration": @5.2}]; +``` + + + +#### Java + +**Syntax** + +```java +static void trackExperienceClick(String experienceId) +static void trackExperienceClick(String experienceId, String experienceLocation) +static void trackExperienceClick(String experienceId, String experienceLocation, Map additionalData) + +``` + +**Example** + +```java +ContentAnalytics.trackExperienceClick( + expId, + "homepage.hero", + Map.of("viewDuration", 5.2) +); +``` + +#### Kotlin + +**Syntax** + +```java +fun trackExperienceClick( + experienceId: String, + experienceLocation: String? = null, + additionalData: Map? = null +): Unit +``` + +**Example** + +```java +// Using InteractionType enum directly +ContentAnalytics.trackExperienceClick( + experienceId: expId, + experienceLocation: "homepage.hero", + additionalData: ["viewDuration": 5.2] +) +``` + + + +#### Swift + +**Syntax** + +```swift +static func trackExperienceClick( + experienceId: String, + experienceLocation: String? = nil, + additionalData: [String: Any]? = nil +) +``` + +**Example** + +```swift +ContentAnalytics.trackExperienceClick( + experienceId: expId, + experienceLocation: "homepage.hero", + additionalData: ["viewDuration": 5.2] +) +``` + +#### Objective-C + +**Syntax** + +```objc ++ (void)trackExperienceClick:(NSString *)assetURL + assetLocation:(nullable NSString *)assetLocation + additionalData:(nullable NSDictionary *)additionalData; + +``` + +**Example** + +```objc +[AEPContentAnalytics trackExperienceClick:expId + experienceLocation:@"homepage.hero" + additionalData:@{@"viewDuration": @5.2}]; +``` + + + +#### Java + +**Syntax** + +```java +public class ContentItem { + private final String value; + private final Map styles; + + public ContentItem(String value, Map styles) { + this.value = value; + this.styles = styles; + } + + // Convenience constructor to mirror the default parameter + public ContentItem(String value) { + this(value, Collections.emptyMap()); + } + + public String getValue() { + return value; + } + + public Map getStyles() { + return styles; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("value", value); + map.put("styles", styles); + return map; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ContentItem)) return false; + ContentItem that = (ContentItem) o; + return Objects.equals(value, that.value) && + Objects.equals(styles, that.styles); + } + + @Override + public int hashCode() { + return Objects.hash(value, styles); + } + + @Override + public String toString() { + return "ContentItem{value='" + value + "', styles=" + styles + "}"; + } +} +``` + +**Example** + +```java +// Asset with URL +ContentItem("https://example.com/hero.jpg") + +// Text with role +ContentItem("Welcome!", mapOf("role" to "headline")) + +// CTA with enabled state +ContentItem("Shop Now", mapOf("enabled" to true, "role" to "primary")) +``` + +#### Kotlin + +**Syntax** + +```java +data class ContentItem( + val value: String, + val styles: Map = emptyMap() +) { + fun toMap(): Map +} +``` + +**Example** + +```java +// Asset with URL +ContentItem(value: "https://example.com/hero.jpg") + +// Text with role +ContentItem(value: "Welcome!", styles: mapOf("role" to "headline")) + +// CTA with enabled state +ContentItem(value: "Shop Now", styles: mapOf("enabled" to true, "role" to "primary")) +``` + + + +#### Swift + +**Syntax** + +```swift +public struct ContentItem { + let value: String + let styles: [String: Any] + + init(value: String, styles: [String: Any]) +} +``` + +**Example** + +```swift +// Asset +ContentItem(value: "https://example.com/image.jpg", styles: [:]) + +// Text with role +ContentItem(value: "Product Title", styles: ["role": "headline"]) +ContentItem(value: "$99.99", styles: ["role": "price"]) + +// CTA +ContentItem(value: "Buy Now", styles: ["enabled": true]) +``` + +#### Objective-C + +**Syntax** + +```objc +@implementation AEPContentItem + +- (instancetype)initWithValue:(NSString *)value + styles:(NSDictionary *)styles { + if (self = [super init]) { + _value = value; + _styles = styles; + } + return self; +} +``` + +**Example** + +```objc +AEPContentItem *asset = [[AEPContentItem alloc] initWithValue:@"https://example.com/image.jpg" + styles:@{}]; + + +AEPContentItem *title = [[AEPContentItem alloc] initWithValue:@"Product Title" + styles:@{@"role": @"headline"}]; +AEPContentItem *price = [[AEPContentItem alloc] initWithValue:@"$99.99" + styles:@{@"role": @"price"}]; + + +AEPContentItem *cta = [[AEPContentItem alloc] initWithValue:@"Buy Now" + styles:@{@"enabled": @YES}]; +``` + + + +#### Java + +**Syntax** + +```java +public enum InteractionType { + VIEW, + CLICK, + DEFINITION; + + public String getStringValue() { + return name().toLowerCase(); + } +} +``` + +**Example** + +```java +ContentAnalytics.trackAsset( + "https://example.com/hero.jpg", + InteractionType.VIEW +) +``` + +#### Kotlin + +**Syntax** + +```java +enum class InteractionType { + VIEW, + CLICK, + DEFINITION; + + val stringValue: String + get() = name.lowercase() +} +``` + +**Example** + +```java +ContentAnalytics.trackAsset( + assetURL = "https://example.com/hero.jpg", + interactionType = InteractionType.VIEW +) +``` + + + +#### Swift + +**Syntax** + +```swift +public enum InteractionType: Int { + case view = 0 + case click = 1 + + public var stringValue: String { ... } + public static func from(string: String) -> InteractionType? +} +``` + +**Example** + +```swift +ContentAnalytics.trackAsset( + assetURL: "https://example.com/hero.jpg", + interactionType: .view +) +``` + +#### Objective-C + +**Syntax** + +```objc +@objc(AEPInteractionType) +public enum InteractionType: Int { + case view = 0 + case click = 1 + + public var stringValue: String { ... } + public static func from(string: String) -> InteractionType? +} +``` + +**Example** + +```objc +[ContentAnalytics trackAsset:@"https://example.com/hero.jpg" + interactionType:AEPInteractionTypeView + assetLocation:nil + additionalData:nil]; +``` diff --git a/src/pages/solution/adobe-content-analytics/tabs/crash-recovery.md b/src/pages/solution/adobe-content-analytics/tabs/crash-recovery.md new file mode 100644 index 0000000000..ae0f30599b --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/tabs/crash-recovery.md @@ -0,0 +1,100 @@ +--- +noIndex: true +--- + +import Alerts from '/src/pages/resources/alerts.md' + + + +```java +fun addAssetEvent(event: Event) + ├─> assetHitProcessor.accumulateEvent(event) // Add to memory + ├─> persistEventImmediately(event, queue) // Write to disk + └─> checkAndFlushIfNeeded() // Check thresholds + +suspend fun performFlush() + ├─> val events = assetHitProcessor.processAccumulatedEvents() + └─> [Orchestrator processes events → dispatches to Edge] + └─> Edge guarantees delivery from here +``` + + + +```swift +func addAssetEvent(_ event: Event) + ├─> assetHitProcessor.accumulateEvent(event) // Add to memory + ├─> persistEventImmediately(event, to: queue) // Write to disk + └─> checkAndFlushIfNeeded() // Check thresholds + +func performFlush() + ├─> let events = assetHitProcessor.processAccumulatedEvents() + └─> [Orchestrator processes events → dispatches to Edge] + └─> Edge guarantees delivery from here +``` + + + +```java +override suspend fun processHit(entity: DataEntity): Boolean + ├─> Decode event from disk + ├─> Accumulate in memory (if not already present) + └─> return true → clear from disk (event now in memory) +``` + + + +```swift +func processHit(entity: DataEntity, completion: (Bool) -> Void) + ├─> Decode event from disk + ├─> Accumulate in memory (if not already present) + └─> completion(true) → clear from disk (event now in memory) +``` + + + +```java +// On flush (ContentAnalyticsOrchestrator.kt) +private fun buildAssetMetricsCollection(events: List): AssetMetricsCollection { + val groupedEvents = events.groupBy { it.assetKey ?: "" } + val metricsMap = mutableMapOf() + + for ((key, events) in groupedEvents) { + val views = events.count { it.interactionType == InteractionType.VIEW } + val clicks = events.count { it.interactionType == InteractionType.CLICK } + metricsMap[key] = AssetMetrics(viewCount = views, clickCount = clicks, ...) + } + + return AssetMetricsCollection(metricsMap) +} +``` + + + +```swift +// On flush (ContentAnalyticsOrchestrator.swift) +func buildAssetMetricsCollection(from events: [Event]) -> AssetMetricsCollection { + let groupedEvents = Dictionary(grouping: events) { $0.assetKey ?? "" } + var metricsMap: [String: AssetMetrics] = [:] + + for (key, events) in groupedEvents { + let views = events.filter { $0.interactionType == .view }.count + let clicks = events.filter { $0.interactionType == .click }.count + metricsMap[key] = AssetMetrics(viewCount: views, clickCount: clicks, ...) + } + + return AssetMetricsCollection(metrics: metricsMap) +} +``` + + + +```java +// BatchCoordinator +private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) +private val stateMutex = kotlinx.coroutines.sync.Mutex() + +// DirectHitProcessor +private val mutex = Mutex() + +// All state mutations wrapped in mutex.withLock { } +``` diff --git a/src/pages/solution/adobe-content-analytics/tabs/experience-tracking.md b/src/pages/solution/adobe-content-analytics/tabs/experience-tracking.md new file mode 100644 index 0000000000..8d750ae7ab --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/tabs/experience-tracking.md @@ -0,0 +1,852 @@ +--- +noIndex: true +--- + +import Alerts from '/src/pages/resources/alerts.md' + + + +```java +// 1. Register (once per experience) +val expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem("https://example.com/hero.jpg", emptyMap())), + texts = listOf(ContentItem("Buy Now", mapOf("role" to "headline"))), + ctas = listOf(ContentItem("Shop", mapOf("enabled" to true))) +) + +// 2. Track view (when visible) +ContentAnalytics.trackExperienceView(expId, "homepage.hero") + +// 3. Track click (on tap) +ContentAnalytics.trackExperienceClick(expId, "homepage.hero") +``` + + + +```swift +// 1. Register (once per experience) +let expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: "https://example.com/hero.jpg", styles: [:])], + texts: [ContentItem(value: "Buy Now", styles: ["role": "headline"])], + ctas: [ContentItem(value: "Shop", styles: ["enabled": true])] +) + +// 2. Track view (when visible) +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "homepage.hero") + +// 3. Track click (on tap) +ContentAnalytics.trackExperienceClick(experienceId: expId, experienceLocation: "homepage.hero") +``` + + + +```swift +let experienceId = ContentAnalytics.registerExperience( + assets: [ + ContentItem(value: "https://example.com/hero.jpg", styles: [:]), + ContentItem(value: "https://example.com/icon.png", styles: [:]) + ], + texts: [ + ContentItem(value: "iPhone 16 Pro", styles: ["role": "headline"]), + ContentItem(value: "Forged in titanium", styles: ["role": "body"]), + ContentItem(value: "$999", styles: ["role": "price"]) + ], + ctas: [ + ContentItem(value: "Buy Now", styles: ["enabled": true]) + ] +) +``` + + + +```java +val experienceId = ContentAnalytics.registerExperience( + assets = listOf( + ContentItem("https://example.com/hero.jpg", emptyMap()), + ContentItem("https://example.com/icon.png", emptyMap()) + ), + texts = listOf( + ContentItem("iPhone 16 Pro", mapOf("role" to "headline")), + ContentItem("Forged in titanium", mapOf("role" to "body")), + ContentItem("$999", mapOf("role" to "price")) + ), + ctas = listOf( + ContentItem("Buy Now", mapOf("enabled" to true)) + ) +) +``` + + + +```swift +ContentAnalytics.trackExperienceView(experienceId: experienceId, experienceLocation: "product.detail") +ContentAnalytics.trackExperienceClick(experienceId: experienceId, experienceLocation: "product.detail") +``` + + + +```java +ContentAnalytics.trackExperienceView(experienceId, "product.detail") +ContentAnalytics.trackExperienceClick(experienceId, "product.detail") +``` + + + +```swift +// Each app session +let expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: "https://example.com/hero.jpg", styles: [:])], + texts: [ContentItem(value: "Title", styles: ["role": "headline"])] +) +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "home") +``` + + + +```java +// Each app session +val expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem("https://example.com/hero.jpg", emptyMap())), + texts = listOf(ContentItem("Title", mapOf("role" to "headline"))) +) +ContentAnalytics.trackExperienceView(expId, "home") +``` + + + +```swift +class ProductDetailViewController { + var experienceId: String? + + override func viewDidLoad() { + super.viewDidLoad() + + experienceId = ContentAnalytics.registerExperience( + assets: product.imageURLs.map { ContentItem(value: $0, styles: [:]) }, + texts: [ + ContentItem(value: product.name, styles: ["role": "headline"]), + ContentItem(value: product.price, styles: ["role": "price"]) + ], + ctas: [ContentItem(value: "Add to Cart", styles: ["enabled": true])] + ) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if let expId = experienceId { + ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "product.detail.\(product.id)") + } + } + + @IBAction func buyButtonTapped(_ sender: Any) { + if let expId = experienceId { + ContentAnalytics.trackExperienceClick(experienceId: expId, experienceLocation: "product.detail.\(product.id)") + } + } +} +``` + + + +```java +class ProductDetailActivity : AppCompatActivity() { + private var experienceId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_product_detail) + + experienceId = ContentAnalytics.registerExperience( + assets = product.imageURLs.map { ContentItem(it, emptyMap()) }, + texts = listOf( + ContentItem(product.name, mapOf("role" to "headline")), + ContentItem(product.price, mapOf("role" to "price")) + ), + ctas = listOf(ContentItem("Add to Cart", mapOf("enabled" to true))) + ) + } + + override fun onResume() { + super.onResume() + experienceId?.let { expId -> + ContentAnalytics.trackExperienceView(expId, "product.detail.${product.id}") + } + } + + fun onBuyButtonClicked() { + experienceId?.let { expId -> + ContentAnalytics.trackExperienceClick(expId, "product.detail.${product.id}") + } + } +} +``` + + + +```swift +class FeedViewController: UIViewController { + var experienceIds: [String: String] = [:] + + func displayProduct(_ product: Product) { + if experienceIds[product.id] == nil { + let expId = ContentAnalytics.registerExperience( + assets: product.imageURLs.map { ContentItem(value: $0, styles: [:]) }, + texts: [ContentItem(value: product.name, styles: ["role": "headline"])] + ) + experienceIds[product.id] = expId + } + } + + func productCellBecameVisible(_ product: Product) { + if let expId = experienceIds[product.id] { + ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "feed.item.\(product.id)") + } + } +} +``` + + + +```java +class FeedFragment : Fragment() { + private val experienceIds = mutableMapOf() + + fun displayProduct(product: Product) { + if (!experienceIds.containsKey(product.id)) { + val expId = ContentAnalytics.registerExperience( + assets = product.imageURLs.map { ContentItem(it, emptyMap()) }, + texts = listOf(ContentItem(product.name, mapOf("role" to "headline"))) + ) + experienceIds[product.id] = expId + } + } + + fun onProductCellVisible(product: Product) { + experienceIds[product.id]?.let { expId -> + ContentAnalytics.trackExperienceView(expId, "feed.item.${product.id}") + } + } +} +``` + + + +```swift +import CommonCrypto + +func computeExperienceId(texts: [String], assets: [String], ctas: [String]) -> String { + let content = (texts.sorted() + assets.sorted() + ctas.sorted()).joined(separator: "|") + let hash = content.data(using: .utf8)!.sha1Hex() + return "mobile-\(hash.prefix(12))" +} +``` + + + +```java +import java.security.MessageDigest + +fun computeExperienceId(texts: List, assets: List, ctas: List): String { + val content = (texts.sorted() + assets.sorted() + ctas.sorted()).joinToString("|") + val hash = MessageDigest.getInstance("SHA-1") + .digest(content.toByteArray()) + .joinToString("") { "%02x".format(it) } + return "mobile-${hash.take(12)}" +} +``` + + + +```swift +// Wrong +ContentAnalytics.trackExperienceView(experienceId: "exp-123") + +// Correct +let expId = ContentAnalytics.registerExperience(...) +ContentAnalytics.trackExperienceView(experienceId: expId) +``` + + + +```java +// Wrong +ContentAnalytics.trackExperienceView("exp-123", "home") + +// Correct +val expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem("https://example.com/image.jpg", emptyMap())), + texts = listOf(ContentItem("Title", mapOf("role" to "headline"))) +) +ContentAnalytics.trackExperienceView(expId, "home") +``` + + + +```swift +// 1. Register experience with assets +let expId = ContentAnalytics.registerExperience( + assets: [ + ContentItem(value: "https://example.com/hero.jpg", styles: [:]), + ContentItem(value: "https://example.com/thumbnail.jpg", styles: [:]) + ], + texts: [ContentItem(value: "Summer Sale", styles: ["role": "headline"])] +) + +// 2. Track asset view (SDK knows this belongs to the experience above) +ContentAnalytics.trackAssetView(assetURL: "https://example.com/hero.jpg") + +// 3. Track experience interaction +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "homepage") +``` + + + +```java +// 1. Register experience with assets +val expId = ContentAnalytics.registerExperience( + assets = listOf( + ContentItem("https://example.com/hero.jpg", emptyMap()), + ContentItem("https://example.com/thumbnail.jpg", emptyMap()) + ), + texts = listOf(ContentItem("Summer Sale", mapOf("role" to "headline"))) +) + +// 2. Track asset view (SDK knows this belongs to the experience above) +ContentAnalytics.trackAssetView("https://example.com/hero.jpg") + +// 3. Track experience interaction +ContentAnalytics.trackExperienceView(expId, "homepage") +``` + + + +```swift +// Asset tracked standalone - no experience context +ContentAnalytics.trackAssetView(assetURL: "https://example.com/hero.jpg") +``` + + + +```java +// Asset tracked standalone - no experience context +ContentAnalytics.trackAssetView("https://example.com/hero.jpg") +``` + + + +```swift +// Same experience tracked at different locations +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "homepage.hero") +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "product.sidebar") +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "checkout.upsell") +``` + + + +```java +// Same experience tracked at different locations +ContentAnalytics.trackExperienceView(expId, "homepage.hero") +ContentAnalytics.trackExperienceView(expId, "product.sidebar") +ContentAnalytics.trackExperienceView(expId, "checkout.upsell") +``` + + + +```swift +// Track without location for aggregate metrics +ContentAnalytics.trackExperienceView(experienceId: expId) +``` + + + +```java +// Track without location for aggregate metrics +ContentAnalytics.trackExperienceView(expId) +``` + + + +```swift +let heroImage = "https://example.com/hero.jpg" + +// Track per location +ContentAnalytics.trackAssetView(assetURL: heroImage, assetLocation: "homepage") +ContentAnalytics.trackAssetView(assetURL: heroImage, assetLocation: "category.electronics") +ContentAnalytics.trackAssetView(assetURL: heroImage, assetLocation: "search.results") +``` + + + +```java +val heroImage = "https://example.com/hero.jpg" + +// Track per location +ContentAnalytics.trackAssetView(heroImage, "homepage") +ContentAnalytics.trackAssetView(heroImage, "category.electronics") +ContentAnalytics.trackAssetView(heroImage, "search.results") +``` + + + +```swift +// You just track normally - ML attributes are automatic +let expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: "https://example.com/urgency-banner.jpg", styles: [:])], + texts: [ + ContentItem(value: "Only 3 left!", styles: ["role": "headline"]), + ContentItem(value: "Order now before it's gone", styles: ["role": "body"]) + ] +) +// Featurization service detects: persuasion_strategy = "scarcity + urgency" + +ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "product.detail") +``` + + + +```java +// You just track normally - ML attributes are automatic +val expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem("https://example.com/urgency-banner.jpg", emptyMap())), + texts = listOf( + ContentItem("Only 3 left!", mapOf("role" to "headline")), + ContentItem("Order now before it's gone", mapOf("role" to "body")) + ) +) +// Featurization service detects: persuasion_strategy = "scarcity + urgency" + +ContentAnalytics.trackExperienceView(expId, "product.detail") +``` + + + +```swift +// Track asset load time +let loadStart = Date() +// ... load image ... +let loadTime = Date().timeIntervalSince(loadStart) * 1000 // ms + +ContentAnalytics.trackAssetView( + assetURL: imageURL, + assetLocation: "product.gallery", + additionalData: [ + "assetLoadTime": loadTime, // How long to load (ms) + "assetSize": imageData.count, // Bytes + "assetSource": "cdn" // Cache vs CDN + ] +) +``` + + + +```java +// Track asset load time +val loadStart = System.currentTimeMillis() +// ... load image ... +val loadTime = System.currentTimeMillis() - loadStart + +ContentAnalytics.trackAssetView( + assetURL = imageURL, + assetLocation = "product.gallery", + additionalData = mapOf( + "assetLoadTime" to loadTime, // How long to load (ms) + "assetSize" to imageData.size, // Bytes + "assetSource" to "cdn" // Cache vs CDN + ) +) +``` + + + +```swift +class ImageViewController { + var viewStartTime: Date? + var imageURL: String? + + func viewDidAppear() { + viewStartTime = Date() + ContentAnalytics.trackAssetView(assetURL: imageURL!, assetLocation: "gallery") + } + + func viewWillDisappear() { + guard let start = viewStartTime else { return } + let viewDuration = Date().timeIntervalSince(start) * 1000 // ms + + ContentAnalytics.trackAssetClick( + assetURL: imageURL!, + assetLocation: "gallery", + additionalData: [ + "assetViewDuration": viewDuration // Time spent viewing (ms) + ] + ) + } +} +``` + + + +```java +class ImageFragment : Fragment() { + private var viewStartTime: Long = 0 + private var imageURL: String? = null + + override fun onResume() { + super.onResume() + viewStartTime = System.currentTimeMillis() + ContentAnalytics.trackAssetView(imageURL!!, "gallery") + } + + override fun onPause() { + super.onPause() + val viewDuration = System.currentTimeMillis() - viewStartTime + + ContentAnalytics.trackAssetClick( + assetURL = imageURL!!, + assetLocation = "gallery", + additionalData = mapOf( + "assetViewDuration" to viewDuration // Time spent viewing (ms) + ) + ) + } +} +``` + + + +```swift +class ProductCardView { + var expId: String? + var appearTime: Date? + + func onAppear() { + appearTime = Date() + expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: product.imageURL, styles: [:])], + texts: [ContentItem(value: product.name, styles: ["role": "headline"])] + ) + ContentAnalytics.trackExperienceView( + experienceId: expId!, + experienceLocation: "homepage.featured" + ) + } + + func onTap() { + let viewDuration = Date().timeIntervalSince(appearTime!) * 1000 + + ContentAnalytics.trackExperienceClick( + experienceId: expId!, + experienceLocation: "homepage.featured", + additionalData: [ + "experienceViewDuration": viewDuration, // Time before click (ms) + "scrollDepth": currentScrollPercent, // How far user scrolled + "interactionIndex": tapCount // Nth interaction + ] + ) + } +} +``` + + + +```java +@Composable +fun ProductCard(product: Product) { + var expId by remember { mutableStateOf(null) } + var appearTime by remember { mutableStateOf(0L) } + + LaunchedEffect(product.id) { + appearTime = System.currentTimeMillis() + expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem(product.imageUrl, emptyMap())), + texts = listOf(ContentItem(product.name, mapOf("role" to "headline"))) + ) + ContentAnalytics.trackExperienceView(expId!!, "homepage.featured") + } + + Column( + modifier = Modifier.clickable { + val viewDuration = System.currentTimeMillis() - appearTime + + ContentAnalytics.trackExperienceClick( + experienceId = expId!!, + experienceLocation = "homepage.featured", + additionalData = mapOf( + "experienceViewDuration" to viewDuration, // Time before click + "scrollDepth" to currentScrollPercent, // How far scrolled + "interactionIndex" to tapCount // Nth interaction + ) + ) + } + ) { + // ... UI content + } +} +``` + + + +```swift +// In your app delegate or SwiftUI app +import AEPAssurance + +// Start Assurance session (typically via deep link) +Assurance.startSession(url: assuranceDeepLink) +``` + + + +```java +// In your Application class or Activity +import com.adobe.marketing.mobile.Assurance + +// Start Assurance session (typically via deep link) +Assurance.startSession(assuranceDeepLink) +``` + + + +```swift +MobileCore.setLogLevel(.trace) +``` + + + +```java +MobileCore.setLogLevel(LoggingMode.VERBOSE) +``` + + + +```swift +if experienceIds[productId] == nil { + experienceIds[productId] = ContentAnalytics.registerExperience(...) +} +``` + + + +```java +if (!experienceIds.containsKey(productId)) { + experienceIds[productId] = ContentAnalytics.registerExperience( + assets = listOf(ContentItem(product.imageUrl, emptyMap())), + texts = listOf(ContentItem(product.name, mapOf("role" to "headline"))) + ) +} +``` + + + +```swift +class CarouselView: UIView { + private var experienceIds: [Int: String] = [:] + + func configureSlide(_ slide: Slide, at index: Int) { + experienceIds[index] = ContentAnalytics.registerExperience( + assets: [ContentItem(value: slide.imageURL, styles: [:])], + texts: [ContentItem(value: slide.title, styles: ["role": "headline"])], + ctas: slide.ctaText.map { [ContentItem(value: $0, styles: ["enabled": true])] } + ) + } + + func slideDidAppear(at index: Int) { + guard let expId = experienceIds[index] else { return } + ContentAnalytics.trackExperienceView(experienceId: expId, experienceLocation: "home.carousel.\(index)") + } + + func slideWasTapped(at index: Int) { + guard let expId = experienceIds[index] else { return } + ContentAnalytics.trackExperienceClick(experienceId: expId, experienceLocation: "home.carousel.\(index)") + } +} +``` + + + +```java +class CarouselAdapter : RecyclerView.Adapter() { + private val experienceIds = mutableMapOf() + + override fun onBindViewHolder(holder: CarouselViewHolder, position: Int) { + val slide = slides[position] + + experienceIds[position] = ContentAnalytics.registerExperience( + assets = listOf(ContentItem(slide.imageUrl, emptyMap())), + texts = listOf(ContentItem(slide.title, mapOf("role" to "headline"))), + ctas = slide.ctaText?.let { listOf(ContentItem(it, mapOf("enabled" to true))) } + ) + + holder.bind(slide) + } + + override fun onViewAttachedToWindow(holder: CarouselViewHolder) { + experienceIds[holder.adapterPosition]?.let { expId -> + ContentAnalytics.trackExperienceView(expId, "home.carousel.${holder.adapterPosition}") + } + } + + fun onSlideClicked(position: Int) { + experienceIds[position]?.let { expId -> + ContentAnalytics.trackExperienceClick(expId, "home.carousel.$position") + } + } +} +``` + + + +```swift +struct ProductCard: View { + let product: Product + @State private var expId: String? + + var body: some View { + VStack { + AsyncImage(url: URL(string: product.imageURL)) + Text(product.name) + Text(product.price) + } + .onAppear { + if expId == nil { + expId = ContentAnalytics.registerExperience( + assets: [ContentItem(value: product.imageURL, styles: [:])], + texts: [ + ContentItem(value: product.name, styles: ["role": "headline"]), + ContentItem(value: product.price, styles: ["role": "price"]) + ] + ) + } + if let id = expId { + ContentAnalytics.trackExperienceView(experienceId: id, experienceLocation: "catalog.product.\(product.id)") + } + } + .onTapGesture { + if let id = expId { + ContentAnalytics.trackExperienceClick(experienceId: id, experienceLocation: "catalog.product.\(product.id)") + } + } + } +} +``` + + + +```java +@Composable +fun ProductCard(product: Product) { + var expId by remember { mutableStateOf(null) } + + LaunchedEffect(product.id) { + expId = ContentAnalytics.registerExperience( + assets = listOf(ContentItem(product.imageUrl, emptyMap())), + texts = listOf( + ContentItem(product.name, mapOf("role" to "headline")), + ContentItem(product.price, mapOf("role" to "price")) + ) + ) + expId?.let { + ContentAnalytics.trackExperienceView(it, "catalog.product.${product.id}") + } + } + + Column( + modifier = Modifier.clickable { + expId?.let { + ContentAnalytics.trackExperienceClick(it, "catalog.product.${product.id}") + } + } + ) { + AsyncImage(model = product.imageUrl, contentDescription = null) + Text(product.name) + Text(product.price) + } +} +``` + + + +```swift +struct TrackedExperience: View { + let assets: [ContentItem] + let texts: [ContentItem] + let location: String + let content: Content + + @State private var expId: String? + + init( + assets: [ContentItem], + texts: [ContentItem], + location: String, + @ViewBuilder content: () -> Content + ) { + self.assets = assets + self.texts = texts + self.location = location + self.content = content() + } + + var body: some View { + content + .onAppear { + if expId == nil { + expId = ContentAnalytics.registerExperience(assets: assets, texts: texts) + } + if let id = expId { + ContentAnalytics.trackExperienceView(experienceId: id, experienceLocation: location) + } + } + .onTapGesture { + if let id = expId { + ContentAnalytics.trackExperienceClick(experienceId: id, experienceLocation: location) + } + } + } +} + +// Usage +TrackedExperience( + assets: [ContentItem(value: product.imageURL, styles: [:])], + texts: [ContentItem(value: product.name, styles: ["role": "headline"])], + location: "product.\(product.id)" +) { + ProductCardView(product: product) +} +``` + + + +```java +@Composable +fun TrackedExperience( + assets: List, + texts: List, + location: String, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit +) { + var expId by remember { mutableStateOf(null) } + + LaunchedEffect(location) { + expId = ContentAnalytics.registerExperience(assets = assets, texts = texts) + expId?.let { ContentAnalytics.trackExperienceView(it, location) } + } + + Box( + modifier = Modifier.clickable { + expId?.let { ContentAnalytics.trackExperienceClick(it, location) } + onClick?.invoke() + } + ) { + content() + } +} + +// Usage +TrackedExperience( + assets = listOf(ContentItem(product.imageUrl, emptyMap())), + texts = listOf(ContentItem(product.name, mapOf("role" to "headline"))), + location = "product.${product.id}" +) { + ProductCardView(product) +} +``` diff --git a/src/pages/solution/adobe-content-analytics/tabs/index.md b/src/pages/solution/adobe-content-analytics/tabs/index.md new file mode 100644 index 0000000000..47190329af --- /dev/null +++ b/src/pages/solution/adobe-content-analytics/tabs/index.md @@ -0,0 +1,72 @@ +--- +noIndex: true +--- + +import Alerts from '/src/pages/resources/alerts.md' + + + +Add the required dependencies to your project by including them in the app's Gradle file. + +```kotlin +// Use the BOM to manage Adobe Mobile SDK versions +implementation(platform("com.adobe.marketing.mobile:sdk-bom:3.+")) + +// Adobe Mobile SDK dependencies (versions managed by BOM) +implementation("com.adobe.marketing.mobile:core") +implementation("com.adobe.marketing.mobile:edge") + +// Content Analytics (not yet in BOM - specify version explicitly) +implementation("com.adobe.marketing.mobile:contentanalytics:1.0.0") +``` + + + + + +Add the required dependencies to your project by including them in the app's Gradle file. + +```java +// Use the BOM to manage Adobe Mobile SDK versions + +implementation platform('com.adobe.marketing.mobile:sdk-bom:3.+') + +// Adobe Mobile SDK dependencies (versions managed by BOM) +implementation 'com.adobe.marketing.mobile:core' +implementation 'com.adobe.marketing.mobile:edge' + +// Content Analytics (not yet in BOM - specify version explicitly) +implementation 'com.adobe.marketing.mobile:contentanalytics:1.0.0" +``` + + + + + +Add the required dependencies to your project using CocoaPods. Add following pods in your `Podfile`: + +```swift +use_frameworks! + +target 'YourTargetApp' do + pod 'AEPCore', '~> 5.0' + pod 'AEPEdge', '~> 5.0' + pod 'AEPContentAnalytics', '~> 5.0' +end +``` + + + +Add the required dependencies to your project using Swift Package Manager. For Content Analytics, use the following instructions. + +1. In Xcode, select **File** > **Add Package Dependencies**. + +1. Enter the package URL: + + ```text + https://github.com/adobe/aca-mobile-sdk-ios-extension + ``` + +1. Select version `5.0.0` or later. + +1. Select **Add Package**. diff --git a/src/pages/solution/index.md b/src/pages/solution/index.md index f4dfd6dfa3..040879cef4 100644 --- a/src/pages/solution/index.md +++ b/src/pages/solution/index.md @@ -12,6 +12,7 @@ Solution extensions are extensions that directly connect with Experience Cloud s ## Extensions * [Adobe Analytics](./adobe-analytics/index.md) +* [Adobe Content Analytics](./adobe-content-analytics/index.md) * [Adobe Audience Manager](./adobe-audience-manager/index.md) * [Adobe Campaign Classic](./adobe-campaign-classic/index.md) * [Adobe Campaign Standard](./adobe-campaign-standard/index.md)