diff --git a/DestinationFilters.md b/DestinationFilters.md new file mode 100644 index 0000000..2c13011 --- /dev/null +++ b/DestinationFilters.md @@ -0,0 +1,217 @@ +# Destination Filters Guide + +Destination Filters brings Segment's powerful server-side filtering capability directly to your mobile app. This feature allows you to filter analytics events on-device using the same JavaScript filtering logic as your server-side implementation, ensuring consistent filtering behavior across your entire analytics pipeline. + +## Core Benefits + +- **Consistent Filtering**: Uses the same JavaScript logic as Segment's server-side filters +- **Device-Mode Support**: Extends filtering capabilities to device-mode destinations +- **Network Optimization**: Filters events before transmission, reducing bandwidth usage +- **Data Point Savings**: Prevents unwanted events from being sent, reducing consumed data points +- **Zero Code Changes**: Configured entirely through the Segment web app + +## How It Works + +Destination Filters downloads your workspace's filtering rules and applies them locally on the device. When an analytics event is generated: + +1. **Event Generated**: Your app creates a track, screen, identify, or group event +2. **Filters Applied**: The event is checked against your downloaded filtering rules +3. **Destination Routing**: Only events that pass the filters are sent to their respective destinations +4. **Consistent Logic**: The same filtering logic runs on-device and server-side + +This ensures that device-mode destinations (like Firebase, Amplitude, etc.) receive exactly the same filtered events as your server-side destinations. + +## Setup + +### Prerequisites + +1. **Segment Workspace**: Destination Filters enabled on your workspace +2. **Web App Configuration**: Filters configured at https://app.segment.com +3. **Analytics-Swift**: Version 1.8.0 or higher + +> **Note**: Destination Filters may need to be enabled on your workspace by your Segment representative. + +### Basic Setup + +```swift +import AnalyticsLive + +let filters = DestinationFilters() +Analytics.main.add(plugin: filters) +``` + +That's it! The plugin automatically: +- Downloads your workspace's filtering rules +- Applies them to all events +- Updates rules when your workspace configuration changes + +### Complete Setup Example + +```swift +import AnalyticsLive + +// Set up your analytics instance as usual +let analytics = Analytics(configuration: Configuration( + writeKey: "" +)) + +// Add destination filters +let filters = DestinationFilters() +analytics.add(plugin: filters) + +// Add other plugins as needed +let livePlugins = LivePlugins() +analytics.add(plugin: livePlugins) + +// Your app is now filtering events on-device! +``` + +## Configuration + +Destination Filters are configured entirely through the Segment web app - no code changes required. + +### Web App Configuration + +1. **Log into Segment**: Visit https://app.segment.com +2. **Navigate to Destinations**: Select your workspace and go to Destinations +3. **Configure Filters**: Set up filtering rules for your destinations +4. **Test Filters**: Use Segment's testing tools to verify filter behavior +5. **Deploy**: Changes are automatically downloaded to your mobile apps + +### Filter Types + +Destination Filters supports all Segment filter types: + +#### Event Filters +Filter events based on event name, properties, or context: +```javascript +// Example: Only allow purchase events over $50 +event.event === "Purchase" && event.properties.value > 50 +``` + +#### Property Filters +Transform or remove specific properties: +```javascript +// Example: Remove PII from events +delete event.properties.email +delete event.context.traits.phone +``` + +#### Destination-Specific Filters +Apply different filtering logic to different destinations: +```javascript +// Example: Send full data to analytics, limited data to advertising +if (destination.name === "Google Analytics") { + return event +} else if (destination.name === "Facebook Pixel") { + delete event.properties.customer_id + return event +} +``` + +## Benefits and Use Cases + +### Network Optimization + +By filtering events on-device, you reduce: +- **Bandwidth Usage**: Fewer HTTP requests to destination APIs +- **Battery Consumption**: Less network activity +- **Data Costs**: Important for users on limited data plans + +### Data Point Management + +Prevent unwanted events from consuming your destination quotas: +- **Debug Events**: Filter out test/debug events in production +- **Spam Prevention**: Block malformed or excessive events +- **Compliance**: Remove events containing PII for specific destinations + +### Consistent User Experience + +Ensure device-mode and server-side destinations receive identical data: +- **A/B Testing**: Consistent test group assignment across all destinations +- **Feature Flags**: Same feature visibility logic everywhere +- **User Segmentation**: Identical user categorization + +## Advanced Scenarios + +### Development vs Production + +Use different filtering strategies for development and production: + +```javascript +// Example filter that blocks test events in production +if (event.properties.environment === "test" && context.app.version.includes("prod")) { + return null // Block test events in production builds +} +return event +``` + +### Gradual Rollouts + +Implement percentage-based filtering for gradual feature rollouts: + +```javascript +// Example: Send new event type to only 10% of users +if (event.event === "New Feature Used") { + const userId = event.userId || event.anonymousId + const hash = simpleHash(userId) + if (hash % 100 < 10) { + return event // Send to 10% of users + } + return null // Block for 90% of users +} +return event +``` + +### Compliance and Privacy + +Automatically remove sensitive data for specific regions or destinations: + +```javascript +// Example: Remove PII for GDPR compliance +if (event.context.location.country === "Germany" && destination.name === "Marketing Tool") { + delete event.properties.email + delete event.properties.phone + delete event.context.traits.firstName + delete event.context.traits.lastName +} +return event +``` + +## Troubleshooting + +### Filters Not Applied + +If your filters aren't working: + +1. **Check Workspace Settings**: Ensure Destination Filters is enabled +2. **Verify Filter Logic**: Test filters in the Segment web app +3. **App Restart**: Restart your app to download latest filter rules +4. **Network Connectivity**: Ensure the device can reach Segment's CDN + +### Testing Filters + +To verify filters are working: + +1. **Segment Debugger**: Use the live debugger to see filtered events +2. **Destination Analytics**: Check if filtered events appear in destination dashboards +3. **Console Logging**: Add temporary logging to your filter JavaScript +4. **A/B Testing**: Create simple test filters to verify behavior + +### Performance Considerations + +- **Filter Complexity**: Keep filter logic simple for best performance +- **Event Volume**: High-volume apps should test filter performance thoroughly +- **Memory Usage**: Complex filters may increase memory usage slightly + +## Migration from Server-Side Only + +If you're currently using only server-side filters: + +1. **Enable Feature**: Contact your Segment representative to enable Destination Filters +2. **Copy Logic**: Your existing server-side filter logic will work on-device +3. **Test Thoroughly**: Verify consistent behavior between server and device filtering +4. **Monitor Performance**: Watch for any performance impact in your mobile app +5. **Gradual Rollout**: Consider enabling for a subset of users initially + +Your existing filter configurations will automatically apply to device-mode destinations once Destination Filters is enabled. diff --git a/LivePlugins.md b/LivePlugins.md new file mode 100644 index 0000000..636e3dc --- /dev/null +++ b/LivePlugins.md @@ -0,0 +1,400 @@ +# LivePlugins Guide + +LivePlugins revolutionizes how you handle analytics transformations by allowing you to write and deploy JavaScript-based transformation plugins directly to your mobile app. Instead of embedding transformation logic in native Swift code and waiting for app release cycles, you can now update transformation logic instantly via server-side updates. + +## Core Benefits + +- **Instant Updates**: Write and deploy transformation logic in JavaScript without app releases +- **Native Performance**: Optimized JavaScript execution with no network access during plugin runtime +- **Seamless Integration**: Works with your existing Analytics-Swift implementation +- **Flexible Targeting**: Apply transformations globally or to specific destinations + +## Setup + +### Basic Setup +```swift +import AnalyticsLive + +let livePlugins = LivePlugins() +Analytics.main.add(plugin: livePlugins) +``` + +### Setup with Fallback +Provide a local JavaScript file as fallback when server downloads fail: + +```swift +let fallbackURL = Bundle.main.url(forResource: "fallback", withExtension: "js") +let livePlugins = LivePlugins(fallbackFileURL: fallbackURL) +Analytics.main.add(plugin: livePlugins) +``` + +**Important**: If using with Signals, LivePlugins must be added first as Signals depends on it. + +## Configuration + +LivePlugins can be configured with several options during initialization: + +```swift +let livePlugins = LivePlugins( + fallbackFileURL: fallbackURL, + force: false, + exceptionHandler: { error in + // Handle JavaScript errors + } +) +``` + +### Configuration Parameters + +#### fallbackFileURL: URL? +- Local file URL containing JavaScript to use as fallback +- Used when server downloads fail or are unavailable +- Ensures basic operation when network connectivity is poor +- **Must be a local file URL** (e.g., from Bundle.main) +- Optional - can be nil if no fallback is needed + +#### force: Bool +- When `true`, always uses the fallback file instead of downloading from server +- When `false` (default), attempts server download first, falls back to local file if needed +- Primarily useful for debugging and development + +#### exceptionHandler: ((Error) -> Void)? +- Custom error handling for JavaScript runtime errors +- Called when JavaScript execution encounters exceptions +- Optional - default behavior logs errors internally + +### Configuration Examples + +#### Basic Setup +```swift +let fallbackURL = Bundle.main.url(forResource: "fallback", withExtension: "js") +let livePlugins = LivePlugins(fallbackFileURL: fallbackURL) +Analytics.main.add(plugin: livePlugins) +``` + +#### Debug/Development Setup +```swift +let fallbackURL = Bundle.main.url(forResource: "testPlugins", withExtension: "js") +let livePlugins = LivePlugins( + fallbackFileURL: fallbackURL, + force: true, // Force local file for debugging + exceptionHandler: { error in + print("LivePlugin error: \(error)") + // Send to crash reporting, show alert, etc. + } +) +Analytics.main.add(plugin: livePlugins) +``` + +## JavaScript Environment + +LivePlugins runs in a secure, sandboxed JavaScript environment with the following characteristics: + +- **No network access** - Plugins cannot make HTTP requests +- **Basic JavaScript functionality** - Standard JavaScript features available +- **Console logging** - Use `console.log()` for debugging +- **Pre-defined analytics instance** - Access via `analytics` variable + +### Pre-defined Variables + +#### analytics +A JavaScript wrapper around your existing native Analytics instance: +```javascript +// This is automatically available and uses your native Analytics configuration +analytics.track("Event Name", { property: "value" }) + +// You can also create additional instances for different write keys +let otherAnalytics = new Analytics("") +``` + +## Plugin Development + +### Plugin Types + +LivePlugins can be created with different types that determine when they run in the analytics timeline: + +- `before` - Runs before any processing occurs +- `enrichment` - Runs during the main processing phase +- `after` - Runs after all processing is complete +- `utility` - Helper plugins that don't directly process events + +### Basic Plugin Structure + +All plugins extend the `LivePlugin` class: + +```javascript +class MyPlugin extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, null) + } + + track(event) { + // Transform track events + return event + } +} + +// Create and add the plugin +let myPlugin = new MyPlugin() +analytics.add(myPlugin) +``` + +### Plugin Lifecycle Methods + +LivePlugins can implement several lifecycle methods: + +#### Event Processing Methods +- `process(event)` - Called for all events (catch-all method) +- `track(event)` - Called for track events specifically +- `identify(event)` - Called for identify events specifically +- `screen(event)` - Called for screen events specifically +- `group(event)` - Called for group events specifically + +#### System Methods +- `update(settings, type)` - Called when settings are updated (type: "Initial" or "Refresh") +- `reset()` - Called when analytics is reset +- `flush()` - Called when analytics is flushed + +### Basic Examples + +#### Fix Misnamed Properties +```javascript +class FixProductViewed extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, null) + } + + track(event) { + if (event.event == "Product Viewed") { + // Set the correct property name + event.properties.product_id = event.properties.product_did + // Remove old property + delete event.properties.product_did + } + return event + } +} + +let productViewFix = new FixProductViewed() +analytics.add(productViewFix) +``` + +#### Add Timestamp to All Events +```javascript +class AddTimestamp extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, null) + } + + process(event) { + event.properties = event.properties || {} + event.properties.processed_at = new Date().toISOString() + return event + } +} + +let timestampPlugin = new AddTimestamp() +analytics.add(timestampPlugin) +``` + +#### Conditional Event Blocking +```javascript +class BlockTestEvents extends LivePlugin { + constructor() { + super(LivePluginType.before, null) + } + + track(event) { + // Block events with test_ prefix + if (event.event.startsWith("test_")) { + return null // Returning null blocks the event + } + return event + } +} + +let blockTestEvents = new BlockTestEvents() +analytics.add(blockTestEvents) +``` + +## Destination-Specific Plugins + +Plugins can target specific destinations by specifying a destination key in the constructor: + +```javascript +// Remove advertisingId only from Amplitude events +class RemoveAdvertisingId extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, "Amplitude") + } + + process(event) { + delete event.context.device.advertisingId + return event + } +} + +let deleteAdID = new RemoveAdvertisingId() +analytics.add(deleteAdID) +``` + +## Advanced Features + +### Context Enrichment + +All analytics methods accept an optional context parameter that gets merged with the event's existing context: + +```javascript +class ContextEnricher extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, null) + } + + track(event) { + // Add context using the analytics instance + analytics.track(event.event, event.properties, { + campaign: { + source: "liveplugin", + version: "1.0" + } + }) + + // Block original event since we sent an enriched version + return null + } +} + +let enricher = new ContextEnricher() +analytics.add(enricher) +``` + +### Settings-Aware Plugins + +Plugins can respond to settings updates: + +```javascript +class SettingsAwarePlugin extends LivePlugin { + constructor() { + super(LivePluginType.enrichment, null) + this.enabled = true + } + + update(settings, type) { + // type is "Initial" or "Refresh" + this.enabled = settings.integrations?.MyPlugin?.enabled ?? true + console.log(`Plugin ${this.enabled ? 'enabled' : 'disabled'} via ${type} settings`) + } + + track(event) { + if (!this.enabled) { + return event // Pass through unchanged + } + + // Apply transformations when enabled + event.properties.plugin_processed = true + return event + } +} + +let settingsPlugin = new SettingsAwarePlugin() +analytics.add(settingsPlugin) +``` + +## JavaScript API Reference + +### Analytics Class +```javascript +class Analytics { + constructor(writeKey) {} // Create new Analytics instance + + // Properties + get traits() {} // Get current user traits + get userId() {} // Get current user ID + get anonymousId() {} // Get current anonymous ID + + // Event Methods + track(event, properties, context) {} // Send track event + identify(userId, traits, context) {} // Send identify event + screen(title, category, properties, context) {} // Send screen event + group(groupId, traits, context) {} // Send group event + + // System Methods + reset() {} // Reset analytics state + flush() {} // Flush queued events + add(livePlugin) {} // Add a LivePlugin +} +``` + +### LivePlugin Class +```javascript +class LivePlugin { + constructor(type, destination) {} // Create plugin with type and optional destination + + // Properties + get type() {} // Get plugin type + get destination() {} // Get destination key or null + + // Lifecycle Methods + update(settings, type) {} // Handle settings updates (type: "Initial" or "Refresh") + process(event) {} // Process any event type + + // Event Methods + track(event) {} // Handle track events + identify(event) {} // Handle identify events + group(event) {} // Handle group events + screen(event) {} // Handle screen events + + // System Methods + reset() {} // Handle analytics reset + flush() {} // Handle analytics flush +} +``` + +### Constants + +#### LivePluginType +Available plugin types: +```javascript +const LivePluginType = { + before: "before", // Run before processing + enrichment: "enrichment", // Run during processing + after: "after", // Run after processing + utility: "utility" // Utility plugins +} +``` + +#### UpdateType +Settings update type values: +- `"Initial"` - Initial settings load +- `"Refresh"` - Settings refresh + +### Utility Functions + +#### Console Logging +```javascript +console.log(message) +``` +Log a message to the native console for debugging. + +## Best Practices + +### Performance +- Keep plugin logic lightweight - they run on every event +- Avoid complex computations in hot paths +- Use early returns to skip unnecessary processing + +### Error Handling +- Always return an event object from processing methods (or null to block) +- Handle undefined/null properties gracefully +- Use console.log() for debugging - logs appear in Xcode console + +### Testing +- Use `force: true` during development to test local JavaScript files +- Implement custom exception handlers to catch errors during development +- Test both global and destination-specific plugin behavior + +### Fallback Strategy +- Always provide a fallback file for critical transformations +- Keep fallback files simple and focused on essential transformations +- Test offline scenarios to ensure fallback behavior works correctly + diff --git a/README.md b/README.md index 08ac5eb..302ae2b 100755 --- a/README.md +++ b/README.md @@ -1,832 +1,129 @@ # Analytics Live -Analytics Live is a powerful enhancement suite that enables dynamic Analytics-Swift, and gives Segment customers the ability to deploy Destination Filters, transformations and even Auto-Instrumentation without app updates, while maintaining enterprise-grade security and performance. +Analytics Live is a powerful enhancement suite for Analytics-Swift that enables dynamic analytics capabilities without app updates. Deploy JavaScript transformations, capture automated user activity signals, and filter events on-device while maintaining enterprise-grade security and performance. -## Core Features +## Features ### 📱 LivePlugins -LivePlugins revolutionizes how you handle analytics transformations by allowing you to write and deploy JavaScript-based transformation plugins directly to your mobile app. +Write and deploy JavaScript-based transformation plugins directly to your mobile app. Update analytics logic instantly via server-side updates without waiting for app releases. -Instead of embedding transformation logic in native Swift code and waiting for app release cycles, you can now: +**[→ Full LivePlugins Guide](LivePlugins.md)** -- Write and update transformation logic in JavaScript -- Deploy changes instantly via server-side updates -- Maintain native-level performance with optimized JavaScript execution -- Seamlessly integrate with your existing analytics implementation +### 🔍 Signals +Automated user activity tracking through a sophisticated signal system. Captures user interactions, navigation, and network activity, then transforms them into meaningful analytics events using JavaScript. -### 🔍 Signals -Signals provides automated user activity tracking through a sophisticated breadcrumb system. It captures crucial user interactions and allows you to transform them into meaningful analytics events using JavaScript. - -**Key Capabilities:** -- **Comprehensive Activity Tracking** - - Navigation events and screen transitions - - User interface interactions - - Network activity monitoring (inbound/outbound) - - Local data access patterns - - Integration with existing analytics events - -- **Enterprise-Grade Privacy** - - Built-in PII protection - - Automatic data obfuscation in release builds - - Configurable privacy rules - -- **Flexible Event Generation** - - Transform breadcrumbs into Segment events using JavaScript - - Create custom event generation rules - - Process and filter data in real-time +**[→ Full Signals Guide](Signals.md)** ### 🎯 Destination Filters -Destination Filters brings Segment's powerful server-side filtering capability directly to your mobile app. This feature: +Brings Segment's server-side filtering capability directly to your mobile app. Filter events on-device using the same JavaScript logic as your server-side implementation. -- Uses the same JavaScript filtering logic as Segment's server-side implementation -- Extends filtering capabilities to device-mode destinations -- Ensures consistent filtering behavior across your entire analytics pipeline -- Optimizes network usage by filtering events before transmission -- Reduces data points consumed by preventing unwanted events from being sent +**[→ Full Destination Filters Guide](DestinationFilters.md)** + +## Installation ### Prerequisites -- Analytics-Swift version 1.6.2 or higher +- Analytics-Swift version 1.8.0 or higher - macOS 10.15+ or iOS 13+ -### Installation - -#### Swift Package Manager +### Swift Package Manager Add Analytics Live as a dependency in your `Package.swift` file: ```swift dependencies: [ - // your existing reference to analytics-swift - .package(url: "https://github.com/segmentio/analytics-swift.git", from: "1.6.2") - // add analytics-live ... - .package(url: "https://github.com/segment-integrations/analytics-swift-live.git", from: "1.0.0") + .package(url: "https://github.com/segmentio/analytics-swift.git", from: "1.8.0"), + .package(url: "https://github.com/segment-integrations/analytics-swift-live.git", from: "3.2.0") ] ``` -#### Xcode + +### Xcode 1. In Xcode, select File → Add Packages... -2. Enter the package URL: https://github.com/segment-integrations/analytics-swift-live.git +2. Enter the package URL: `https://github.com/segment-integrations/analytics-swift-live.git` 3. Click Add Package -### Usage +## Quick Start -Analytics Live integrates with your existing Analytics-Swift setup through three main plugins: +Import Analytics Live in your project: -#### Import AnalyticsLive ```swift import AnalyticsLive ``` -#### Using Live Plugins -LivePlugins allows you to transform analytics data using JavaScript. You can provide a fallback JavaScript file that will be used when network access is unavailable or during first launch. +### LivePlugins Quick Start -```swift -// Set up fallback file -let fallbackURL: URL? = Bundle.main.url(forResource: "myFallback", withExtension: "js") -// Initialize and add LivePlugins -let lp = LivePlugins(fallbackFileURL: fallbackURL) -Analytics.main.add(plugin: lp) -``` - -#### Using Signals -Signals is implemented as a singleton to enable signal emission from anywhere in your app. Basic setup requires only a write key, with additional configuration options covered in the Signals Configuration section. +Transform analytics events with JavaScript: ```swift -let config = SignalsConfiguration(writeKey: "") -Signals.shared.useConfiguration(config) -Analytics.main.add(plugin: Signals.shared) -``` - -#### Using Destination Filters -Destination Filters allows you to filter analytics events on-device using rules configured in the Segment web app. - -```swift -let filters = DestinationFilters() -Analytics.main.add(plugin: filters) -``` - -## LivePlugins - -LivePlugins allows you to write JavaScript-based transformation plugins that process analytics events in your app. These plugins run in a secure, sandboxed JavaScript environment with no network access. Also see the [LivePlugins Javascript API Reference](#liveplugins-javascript-api-reference) for more information. - -### Plugin Types - -LivePlugins can be created with different types that determine when they run in the analytics timeline: -- `before` - Runs before any processing occurs -- `enrichment` - Runs during the main processing phase -- `after` - Runs after all processing is complete -- `utility` - Helper plugins that don't directly process events - -### Basic Example - -Here's a simple plugin that fixes a misnamed property: - -```javascript -class FixProductViewed extends LivePlugin { - track(event) { - if (event.event == "Product Viewed") { - // set the correct property name - event.properties.product_id = event.properties.product_did - // remove old property - delete event.properties.product_did - } - return event - } -} - -// Create and add the plugin -let productViewFix = new FixProductViewed(LivePluginType.enrichment, null) -analytics.add(productViewFix) -``` - -### Destination-Specific Plugins - -Plugins can target specific destinations by specifying a destination key: - -```javascript -// Remove advertisingId only from Amplitude events -class RemoveAdvertisingId extends LivePlugin { - process(event) { - delete event.context.device.advertisingId - return super.process(event) - } -} - -let deleteAdID = new RemoveAdvertisingId(LivePluginType.enrichment, "Amplitude") -analytics.add(deleteAdID) -``` - -### JavaScript Environment - -LivePlugins runs in a limited JavaScript environment: -- No network access -- Basic JavaScript functionality only -- Console logging available via `console.log()` -- Access to analytics instance via pre-defined `analytics` variable - -### Plugin Lifecycle Methods - -LivePlugins can implement several lifecycle methods: -- `process(event)` - Called for all events -- `track(event)` - Called for track events -- `identify(event)` - Called for identify events -- `screen(event)` - Called for screen events -- `group(event)` - Called for group events -- `alias(event)` - Called for alias events -- `update(settings, type)` - Called when settings are updated -- `reset()` - Called when analytics is reset -- `flush()` - Called when analytics is flushed - +// Optional: provide fallback JavaScript file +let fallbackURL = Bundle.main.url(forResource: "fallback", withExtension: "js") -## Signals - -Signals are lightweight data points that capture user interactions and system events in your app. While individual signals provide small pieces of information, they become powerful when combined to generate rich analytics events. Also see the [Signals Javascript API Reference](#signals-javascript-runtime-api) for more information. - -### Core Concepts - -#### What are Signals? -Signals represent discrete app activities, such as: -- Button taps (e.g., "Add To Cart" button clicked) -- Navigation changes (e.g., user entered Product Detail screen) -- Network requests/responses -- User interactions -- System events - -#### Signal Buffer -The Signals system maintains a buffer of recent signals (default: 1000) that can be used by JavaScript event generators. This buffer allows you to: -- Access historical signals -- Correlate related signals -- Build rich context for events - -#### Signal Processing -When signals are emitted, they're processed through your custom signal processing function: - -```javascript -function processSignal(signal) { - trackScreens(signal) - trackAddToCart(signal) -} +let livePlugins = LivePlugins(fallbackFileURL: fallbackURL) +Analytics.main.add(plugin: livePlugins) ``` -This will then reach out to the individual event generators to see if Segment events can be formed. - -#### Event Generators -Event generators are JavaScript functions that process signals and generate Segment events. Here's a simple example that creates screen events: +**[→ See full plugin development guide](LivePlugins.md)** -```javascript -function trackScreens(signal) { - if (signal.type === SignalType.Navigation && signal.data.action === NavigationAction.Entering) { - analytics.screen("Screen Viewed", null, { - "screenName": signal.data.screen - }) - } -} -``` - -#### Advanced Signal Correlation -Event generators can look back through the signal buffer to correlate related signals. Here's an example that combines user interaction with network data: - -```javascript -function trackAddToCart(signal) { - // Check for "Add To Cart" button tap - if (signal.type === SignalType.Interaction && signal.data.title === "Add To Cart") { - let properties = {} - - // Look for recent product network response - const network = signals.find(signal, SignalType.Network, (s) => { - return (s.data.action === NetworkAction.Response && s.data.url.includes("product")) - }) - - // Enrich event with product data - if (network) { - properties.price = network.data.data.content.price - properties.currency = network.data.data.content.currency ?? "USD" - properties.productId = network.data.data.content.id - properties.productName = network.data.data.content.title - } - - // Send enriched track event - analytics.track("Add To Cart", properties) - } -} -``` +### Signals Quick Start -This approach to signal correlation allows you to: -- Build rich, contextual events -- Combine data from multiple sources -- Process data efficiently on-device -- Reduce need for server-side data correlation +Capture and process user activity automatically: -## Signals Configuration - -Signals can be configured through the `SignalsConfiguration` struct, which offers various options to control buffer size, automatic signal generation, network monitoring, and broadcasting behavior. - -### Basic Configuration ```swift -let config = SignalsConfiguration( - writeKey: "", - useSwiftUIAutoSignal: true, - useNetworkAutoSignal: true -) -``` +// Add LivePlugins first (required when using Signals) +let livePlugins = LivePlugins() +Analytics.main.add(plugin: livePlugins) -### Configuration Options - -#### Required Parameters -- `writeKey: String` - - Your Segment write key - - No default value, must be provided - -#### Buffer Control -- `maximumBufferSize: Int` - - Maximum number of signals kept in memory - - Default: 1000 - -#### Signal Relay -- `relayCount: Int` - - Number of signals to collect before processing - - Default: 20 -- `relayInterval: TimeInterval` - - Time interval between signal processing in seconds - - Default: 60 - -#### Automatic Signal Generation -- `useUIKitAutoSignal: Bool` - - Enable automatic UIKit interaction signals - - Default: false -- `useSwiftUIAutoSignal: Bool` - - Enable automatic SwiftUI interaction signals - - Default: false -- `useNetworkAutoSignal: Bool` - - Enable automatic network request/response signals - - Default: false - -#### Network Host Control -- `allowedNetworkHosts: [String]` - - List of hosts to monitor for network signals - - Default: ["*"] (all hosts) -- `blockedNetworkHosts: [String]` - - List of hosts to exclude from network signals - - Default: [] (empty array) - - Automatically includes Segment endpoints: - ``` - api.segment.com - cdn-settings.segment.com - signals.segment.com - api.segment.build - cdn.segment.build - signals.segment.build - ``` - -#### Signal Broadcasting -- `broadcasters: [SignalBroadcaster]?` - - Array of broadcasters to handle signals - - Default: [SegmentBroadcaster()] - - Available broadcasters: - - `SegmentBroadcaster`: Sends signals to Segment when in Debug mode - - `DebugBroadcaster`: Prints signals to the console - - `WebhookBroadcaster`: Delivers signals to a specified webhook URL - -### Example Configurations - -#### Basic Configuration -```swift let config = SignalsConfiguration( - writeKey: "" + writeKey: "", + useSwiftUIAutoSignal: true, + useNetworkAutoSignal: true ) -``` - -#### Full SwiftUI App Configuration -```swift -let config = SignalsConfiguration( - writeKey: "", - maximumBufferSize: 2000, - relayCount: 10, - relayInterval: 30, - useSwiftUIAutoSignal: true, - useNetworkAutoSignal: true -) -``` -Note: For SwiftUI, you'll also need to add these typealiases somewhere in your project to allow interaction signals to be captured. These are thin wrappers over SwiftUI's structs, no UI element behavior changes will occur. - -```swift -typealias Button = SignalButton -typealias NavigationLink = SignalNavigationLink -typealias NavigationStack = SignalNavigationStack -typealias TextField = SignalTextField -typealias SecureField = SignalSecureField -``` - -#### Custom Network Monitoring -```swift -let config = SignalsConfiguration( - writeKey: "", - useNetworkAutoSignal: true, - allowedNetworkHosts: ["api.myapp.com", "api.myanalytics.com"], - blockedNetworkHosts: ["internal.myapp.com"] -) -``` - -## Types of Signals - -All signals share common fields: -- `anonymousId`: The anonymous identifier of the user -- `timestamp`: ISO8601 formatted timestamp of when the signal was created -- `index`: Sequential index of the signal -- `type`: The type of signal (navigation, interaction, network, etc.) -- `data`: Signal-specific data as detailed below - -### Navigation Signals -Captures navigation events within your app. -- `action`: Type of navigation - - `forward`: Forward navigation - - `backward`: Backward navigation - - `modal`: Modal presentation - - `entering`: Entering a screen - - `leaving`: Leaving a screen - - `page`: Page change - - `popup`: Popup display -- `screen`: Name or identifier of the screen - -### Interaction Signals -Captures user interactions with UI components. -- `component`: Type of UI component interacted with -- `title`: Text or identifier associated with the component (optional) -- `data`: Additional contextual data about the interaction in JSON format (optional) - -### Network Signals -Monitors network activity in your app. -- `action`: Type of network activity - - `request`: Outgoing network request - - `response`: Incoming network response -- `url`: The URL being accessed -- `statusCode`: HTTP status code (for responses) -- `data`: Response data in JSON format (optional) - -### Local Data Signals -Monitors interactions with local data storage. -- `action`: Type of data operation - - `loaded`: Data was loaded - - `updated`: Data was updated - - `saved`: Data was saved - - `deleted`: Data was deleted - - `undefined`: Other operations -- `identifier`: Identifier for the data being operated on -- `data`: The data being operated on in JSON format (optional) - -### Instrumentation Signals -Captures analytics events from your existing instrumentation. -- `type`: Type of analytics event - - `track`: Track events - - `screen`: Screen events - - `identify`: Identify events - - `group`: Group events - - `alias`: Alias events - - `unknown`: Unknown event types -- `rawEvent`: The original analytics event data in JSON format - -### User Defined Signals -Create custom signals to capture app-specific events. -- `type`: Always `.userDefined` -- `data`: Custom data structure defined by you - -Example of creating a custom signal: -```swift -struct MyKindaSignal: RawSignal { - struct MyKindaData { - let that: String - } - - var anonymousId: String = Signals.shared.anonymousId - var type: SignalType = .userDefined - var timestamp: String = Date().iso8601() - var index: Int = Signals.shared.nextIndex - var data: MyKindaData - - init(that: String) { - self.data = MyKindaData(that: that) - } -} - -... - -// Manually emit the signal in your code ... -Signals.emit(myKindaSignal("Robbie Ray Rana") -``` - -## Signal Broadcasting - -Signals are broadcast according to these rules: - -- `SegmentBroadcaster`: Only broadcasts signals to Segment when app is built with DEBUG enabled -- `DebugBroadcaster`: Always broadcasts signals to console, regardless of build configuration -- `WebhookBroadcaster`: Always broadcasts signals to specified webhook URL, regardless of build configuration - -### Custom Broadcasters -You can create your own broadcasters in two ways: - -1. Conform to `SignalBroadcaster` to work with typed signals: -```swift -public protocol SignalBroadcaster { - var analytics: Analytics? { get set } - func added(signal: any RawSignal) - func relay() -} -``` - -2. Conform to `SignalJSONBroadcaster` to work with raw dictionary data: -```swift -public protocol SignalJSONBroadcaster: SignalBroadcaster { - func added(signal: [String: Any]) -} +Signals.shared.useConfiguration(config) +Analytics.main.add(plugin: Signals.shared) ``` -The JSON broadcaster is useful when you need to work with the raw dictionary representation of signals before they're converted to JSON. - -> Note: Signals is a paid feature that may need to be enabled on your workspace by your Segment representative. - -## Destination Filters - -Destination Filters allows you to run your Segment workspace's destination filters directly on-device. This feature requires: +**[→ See full signals configuration and usage](Signals.md)** -1. A Segment workspace with Destination Filters enabled (contact your Segment representative) -2. Destination Filters configured in your workspace at https://app.segment.com +### Destination Filters Quick Start -### Usage - -Add the Destination Filters plugin to your analytics instance: +Filter events on-device using your Segment workspace rules: ```swift let filters = DestinationFilters() Analytics.main.add(plugin: filters) ``` -Once configured, your device-mode destinations will automatically respect the same filtering rules you've set up in your Segment workspace. - -> Note: Destination Filters is a paid feature that may need to be enabled on your workspace by your Segment representative. - -___ - -## LivePlugins Javascript API Reference - -### Utility Functions +**[→ See full destination filters setup](DestinationFilters.md)** -#### Console Logging -```javascript -console.log(message) -``` -Log a message to the native console. - -### Pre-defined Variables - -#### analytics -A pre-configured Analytics instance using your write key: -```javascript -let analytics = Analytics("") -``` - -### Core Classes - -#### Analytics Class -```javascript -class Analytics { - constructor(writeKey) {} // Create new Analytics instance - - // Properties - get traits() {} // Get current user traits - get userId() {} // Get current user ID - get anonymousId() {} // Get current anonymous ID - - // Event Methods - track(event, properties) {} // Send track event - identify(userId, traits) {} // Send identify event - screen(title, category, properties) {} // Send screen event - group(groupId, traits) {} // Send group event - alias(newId) {} // Send alias event - - // System Methods - reset() {} // Reset analytics state - flush() {} // Flush queued events - add(livePlugin) {} // Add a LivePlugin -} -``` - -#### LivePlugin Class -```javascript -class LivePlugin { - constructor(type, destination) {} // Create plugin with type and optional destination - - // Properties - get type() {} // Get plugin type - get destination() {} // Get destination key or null - - // Lifecycle Methods - update(settings, type) {} // Handle settings updates - process(event) {} // Process any event type - - // Event Methods - track(event) {} // Handle track events - identify(event) {} // Handle identify events - group(event) {} // Handle group events - alias(event) {} // Handle alias events - screen(event) {} // Handle screen events - - // System Methods - reset() {} // Handle analytics reset - flush() {} // Handle analytics flush -} -``` - -### Constants - -#### LivePluginType -Available plugin types: -```javascript -const LivePluginType = { - before: "before", // Run before processing - enrichment: "enrichment", // Run during processing - after: "after", // Run after processing - utility: "utility" // Utility plugins -} -``` - -#### UpdateType -Settings update types: -```javascript -const UpdateType = { - initial: true, // Initial settings load - refresh: false // Settings refresh -} -``` - -## Signals JavaScript Runtime API - -### Signal Type Constants - -```javascript -const SignalType = { - Interaction: "interaction", - Navigation: "navigation", - Network: "network", - LocalData: "localData", - Instrumentation: "instrumentation", - UserDefined: "userDefined" -} -``` - -### Base Signal Classes - -#### RawSignal -Base class for all signals: - -```javascript -class RawSignal { - constructor(type, data) {} // Create new signal with type and data - - // Properties - anonymousId: string // Anonymous ID from analytics instance - type: SignalType // Type of signal - data: object // Signal-specific data - timestamp: Date // Creation timestamp - index: number // Sequential index (set by signals.add()) -} -``` - -### Navigation Signals - -Navigation action constants: - -```javascript -const NavigationAction = { - Forward: "forward", // Forward navigation - Backward: "backward", // Backward navigation - Modal: "modal", // Modal presentation - Entering: "entering", // Screen entry - Leaving: "leaving", // Screen exit - Page: "page", // Page change - Popup: "popup" // Popup display -} -``` +## Complete Setup Example -Navigation signal class: +Using all Analytics Live features together: -```javascript -class NavigationSignal extends RawSignal { - constructor(action, screen) {} // Create navigation signal - - // Data Properties - data.action: string // NavigationAction value - data.screen: string // Screen identifier -} -``` - -### Interaction Signals - -```javascript -class InteractionSignal extends RawSignal { - constructor(component, info, object) {} // Create interaction signal - - // Data Properties - data.component: string // UI component type - data.info: string // Additional information - data.data: object // Custom interaction data -} -``` - -### Network Signals - -Network action constants: - -```javascript -const NetworkAction = { - Request: "request", // Outgoing request - Response: "response" // Incoming response -} -``` - -Network signal class: - -```javascript -class NetworkSignal extends RawSignal { - constructor(action, url, object) {} // Create network signal - - // Data Properties - data.action: string // NetworkAction value - data.url: string // Request/response URL - data.data: object // Network payload data -} -``` - -### Local Data Signals - -Local data action constants: - -```javascript -const LocalDataAction = { - Loaded: "loaded", // Data loaded - Updated: "updated", // Data updated - Saved: "saved", // Data saved - Deleted: "deleted", // Data deleted - Undefined: "undefined" // Other operations -} -``` - -Local data signal class: - -```javascript -class LocalDataSignal extends RawSignal { - constructor(action, identifier, object) {} // Create local data signal - - // Data Properties - data.action: string // LocalDataAction value - data.identifier: string // Data identifier - data.data: object // Associated data -} -``` - -### Instrumentation Signals - -Event type constants: - -```javascript -const EventType = { - Track: "track", // Track events - Screen: "screen", // Screen events - Identify: "identify", // Identify events - Group: "group", // Group events - Alias: "alias" // Alias events -} -``` - -Instrumentation signal class: - -```javascript -class InstrumentationSignal extends RawSignal { - constructor(rawEvent) {} // Create instrumentation signal - - // Data Properties - data.type: string // EventType value - data.rawEvent: object // Original analytics event -} -``` +```swift +import AnalyticsLive -### Signals Buffer Management - -The Signals class manages a buffer of recently collected signals: - -```javascript -class Signals { - constructor() {} // Create new signals buffer - - // Properties - signalBuffer: RawSignal[] // Array of signals - signalCounter: number // Current signal count - maxBufferSize: number // Maximum buffer size (default: 1000) - - // Methods - add(signal) {} // Add signal to buffer - getNextIndex() {} // Get next signal index - - // Signal Search Methods - find(fromSignal, // Starting signal (optional) - signalType, // Signal type to find (optional) - predicate) {} // Search predicate function - - findAndApply(fromSignal, // Starting signal (optional) - signalType, // Signal type to find (optional) - searchPredicate, // Search criteria - applyPredicate) {} // Function to apply to found signal -} -``` +// 1. Set up LivePlugins first (required if using with Signals) +let fallbackURL = Bundle.main.url(forResource: "fallback", withExtension: "js") +let livePlugins = LivePlugins(fallbackFileURL: fallbackURL) +Analytics.main.add(plugin: livePlugins) -A global instance is automatically created and available: +// 2. Configure and add Signals +let signalsConfig = SignalsConfiguration(writeKey: "") +Signals.shared.useConfiguration(signalsConfig) +Analytics.main.add(plugin: Signals.shared) -```javascript -let signals = new Signals() // Global signals buffer instance +// 3. Add Destination Filters +let filters = DestinationFilters() +Analytics.main.add(plugin: filters) ``` -### Usage Examples - -Creating and adding signals: +## Documentation -```javascript -// Create navigation signal -let navSignal = new NavigationSignal( - NavigationAction.Entering, - "ProductDetail" -) -signals.add(navSignal) +- **[LivePlugins Guide](LivePlugins.md)** - JavaScript plugin development and API reference +- **[Signals Guide](Signals.md)** - Automated activity tracking and signal processing +- **[Destination Filters Guide](DestinationFilters.md)** - On-device event filtering -// Create interaction signal -let buttonSignal = new InteractionSignal( - "button", - "Add to Cart", - { productId: "123" } -) -signals.add(buttonSignal) -``` +## Support -Finding related signals: - -```javascript -// Find most recent network response -let networkSignal = signals.find( - currentSignal, - SignalType.Network, - (signal) => { - return signal.data.action === NetworkAction.Response - } -) - -// Find and process related signals -signals.findAndApply( - currentSignal, - SignalType.Interaction, - (signal) => signal.data.component === "button", - (found) => { - // Process found signal - console.log("Found related interaction:", found) - } -) -``` \ No newline at end of file +Analytics Live features may require enablement on your Segment workspace. Contact your Segment representative for access to LivePlugins, Signals, or Destination Filters. diff --git a/Signals.md b/Signals.md new file mode 100644 index 0000000..2a73310 --- /dev/null +++ b/Signals.md @@ -0,0 +1,593 @@ +# Signals Guide + +Signals provides automated user activity tracking through a sophisticated breadcrumb system. It captures crucial user interactions and allows you to transform them into meaningful analytics events using JavaScript. + +## Core Concepts + +### What are Signals? +Signals represent discrete app activities, such as: +- Button taps (e.g., "Add To Cart" button clicked) +- Navigation changes (e.g., user entered Product Detail screen) +- Network requests/responses +- User interactions +- System events + +### Signal Buffer +The Signals system maintains a buffer of recent signals (default: 1000) that can be used by JavaScript event generators. This buffer allows you to: +- Access historical signals +- Correlate related signals +- Build rich context for events + +### Signal Processing +When signals are emitted, they're processed through your custom signal processing function: + +```javascript +function processSignal(signal) { + trackScreens(signal) + trackAddToCart(signal) +} +``` + +This will then reach out to the individual event generators to see if Segment events can be formed. + +### Event Generators +Event generators are JavaScript functions that process signals and generate Segment events. Here's a simple example that creates screen events: + +```javascript +function trackScreens(signal) { + if (signal.type === SignalType.Navigation) { + analytics.screen("Screen Viewed", null, { + "screenName": signal.data.currentScreen, + "previousScreen": signal.data.previousScreen + }) + } +} +``` + +### Advanced Signal Correlation +Event generators can look back through the signal buffer to correlate related signals. Here's an example that combines user interaction with network data: + +```javascript +function trackAddToCart(signal) { + // Check for "Add To Cart" button tap + if (signal.type === SignalType.Interaction && signal.data.target.title === "Add To Cart") { + let properties = {} + + // Look for recent product network response + const network = signals.find(signal, SignalType.Network, (s) => { + return (s.data.action === NetworkAction.Response && s.data.url.includes("product")) + }) + + // Enrich event with product data + if (network && network.data.body) { + properties.price = network.data.body.content.price + properties.currency = network.data.body.content.currency ?? "USD" + properties.productId = network.data.body.content.id + properties.productName = network.data.body.content.title + } + + // Send enriched track event + analytics.track("Add To Cart", properties) + } +} +``` + +This approach to signal correlation allows you to: +- Build rich, contextual events +- Combine data from multiple sources +- Process data efficiently on-device +- Reduce need for server-side data correlation + +## Setup + +### Basic Setup +```swift +import AnalyticsLive + +// Add LivePlugins first (required dependency) +let livePlugins = LivePlugins() +Analytics.main.add(plugin: livePlugins) + +// Configure and add Signals +let config = SignalsConfiguration(writeKey: "") +Signals.shared.useConfiguration(config) +Analytics.main.add(plugin: Signals.shared) +``` + +**Important**: LivePlugins must be added before Signals as Signals depends on it. + +## Configuration + +Signals can be configured through the `SignalsConfiguration` struct, which offers various options to control buffer size, automatic signal generation, network monitoring, and debug behavior. + +### Basic Configuration +```swift +let config = SignalsConfiguration( + writeKey: "", + useSwiftUIAutoSignal: true, + useNetworkAutoSignal: true +) +``` + +### Configuration Options + +#### Required Parameters +- `writeKey: String` + - Your Segment write key + - No default value, must be provided + +#### Buffer Control +- `maximumBufferSize: Int` + - Maximum number of signals kept in memory + - Default: 1000 + +#### Signal Relay +- `relayCount: Int` + - Number of signals to collect before sending to broadcasters + - Default: 20 +- `relayInterval: TimeInterval` + - Time interval between sending signals to broadcasters in seconds + - Default: 60 + +#### Automatic Signal Generation +- `useUIKitAutoSignal: Bool` + - Enable automatic UIKit interaction signals + - Default: false +- `useSwiftUIAutoSignal: Bool` + - Enable automatic SwiftUI interaction signals + - Default: false +- `useNetworkAutoSignal: Bool` + - Enable automatic network request/response signals + - Default: false + +#### Debug Signal Transmission +- `sendDebugSignalsToSegment: Bool` + - Send signals to Segment for debugging signal-to-event generators + - Default: false + - **Use only during development when building signal processing logic** + - High signal volume may impact your MTU limits +- `obfuscateDebugSignals: Bool` + - Obfuscate sensitive data in debug signals sent to Segment + - Default: true + - **Note**: If `sendDebugSignalsToSegment=true`, signals will be obfuscated unless you explicitly set `obfuscateDebugSignals=false` + - **Warning**: Disabling obfuscation may expose PII in signal data + +#### Network Host Control +- `allowedNetworkHosts: [String]` + - List of hosts to monitor for network signals + - Default: ["*"] (all hosts) + - Use "*" to allow all hosts, or specify exact hostnames + - Examples: ["api.myapp.com", "analytics.mysite.com"] +- `blockedNetworkHosts: [String]` + - List of hosts to exclude from network signals + - Default: [] (empty array) + - **Blocked hosts always take precedence over allowed hosts** + - Automatically includes Segment endpoints: + ``` + api.segment.com + cdn-settings.segment.com + signals.segment.com + api.segment.build + cdn.segment.build + signals.segment.build + ``` + +#### Network Filtering Rules +Network signal monitoring follows these rules in order: + +1. **Blocked hosts win**: If a host appears in `blockedNetworkHosts`, it will never generate signals +2. **Allowed hosts**: If not blocked, the host must either: + - Be explicitly listed in `allowedNetworkHosts`, OR + - Have "*" in the `allowedNetworkHosts` array (which allows all hosts) +3. **Scheme restriction**: Only HTTP and HTTPS requests are monitored +4. **Host-only matching**: Filtering is based on hostname only (e.g., "api.myapp.com"), not full URLs with paths + +#### Network Monitoring Examples + +**Monitor only specific APIs:** +```swift +let config = SignalsConfiguration( + writeKey: "", + useNetworkAutoSignal: true, + allowedNetworkHosts: ["api.myapp.com", "api.analytics.com"], + blockedNetworkHosts: [] // No additional blocks beyond Segment endpoints +) +``` + +**Monitor all hosts except internal ones:** +```swift +let config = SignalsConfiguration( + writeKey: "", + useNetworkAutoSignal: true, + allowedNetworkHosts: ["*"], // Allow all hosts + blockedNetworkHosts: ["internal.myapp.com", "dev-api.myapp.com"] +) +``` + +**Block specific hosts while allowing others:** +```swift +let config = SignalsConfiguration( + writeKey: "", + useNetworkAutoSignal: true, + allowedNetworkHosts: ["api.myapp.com", "public-api.myapp.com"], + blockedNetworkHosts: ["api.myapp.com"] // This would block api.myapp.com despite being in allowed list +) +// Result: Only public-api.myapp.com would generate signals +``` + +#### Custom Broadcasting +- `broadcasters: [SignalBroadcaster]` + - Array of custom broadcasters to handle signals + - Default: [] (empty array) + - Available broadcasters: + - `DebugBroadcaster`: Prints signal contents to Xcode console + - `WebhookBroadcaster`: Sends signals to a user-supplied webhook URL + - Use for custom signal handling beyond built-in debug transmission + +### Example Configurations + +#### Basic Configuration +```swift +let config = SignalsConfiguration( + writeKey: "" +) +``` + +#### Debug Configuration +```swift +let config = SignalsConfiguration( + writeKey: "", + sendDebugSignalsToSegment: true, + obfuscateDebugSignals: false // Show raw signal data for debugging +) +``` + +#### Full SwiftUI App Configuration +```swift +let config = SignalsConfiguration( + writeKey: "", + maximumBufferSize: 2000, + relayCount: 10, + relayInterval: 30, + useSwiftUIAutoSignal: true, + useNetworkAutoSignal: true +) +``` + +Note: For SwiftUI, you'll also need to add these typealiases somewhere in your project to allow interaction signals to be captured. These are thin wrappers over SwiftUI's structs, no UI element behavior changes will occur. Additional SwiftUI controls will be supported in the future. + +```swift +typealias Button = SignalButton +typealias NavigationLink = SignalNavigationLink +typealias NavigationStack = SignalNavigationStack +typealias TextField = SignalTextField +typealias SecureField = SignalSecureField +``` + +The complete list of available typealiases is maintained in [Typealiases.swift](Sources/AnalyticsLive/Signals/AutoTracking/SwiftUI/Typealiases.swift). + +#### Custom Network Monitoring +```swift +let config = SignalsConfiguration( + writeKey: "", + useNetworkAutoSignal: true, + allowedNetworkHosts: ["api.myapp.com", "api.myanalytics.com"], + blockedNetworkHosts: ["internal.myapp.com"] +) +``` + +## Signal Types + +All signals share common fields: +- `anonymousId`: The anonymous identifier of the user +- `timestamp`: ISO8601 formatted timestamp of when the signal was created +- `index`: Sequential index of the signal +- `type`: The type of signal (navigation, interaction, network, etc.) +- `context`: Static context set at emit time (optional) +- `data`: Signal-specific data as detailed below + +### Navigation Signals +Captures navigation events within your app. +- `currentScreen`: Name or identifier of the current screen +- `previousScreen`: Name or identifier of the previous screen (optional) + +### Interaction Signals +Captures user interactions with UI components using a nested target structure. +- `target.component`: Type of UI component interacted with +- `target.title`: Text or identifier associated with the component (optional) +- `target.data`: Additional contextual data about the interaction in JSON format (optional) + +### Network Signals +Monitors network activity in your app with comprehensive request/response data. +- `action`: Type of network activity (`request` or `response`) +- `url`: The URL being accessed +- `body`: Request/response body in JSON format (optional) +- `contentType`: Content type of the request/response (optional) +- `method`: HTTP method (GET, POST, etc.) (optional) +- `status`: HTTP status code (optional) +- `ok`: Boolean indicating if status code represents success (optional) +- `requestId`: Unique identifier linking requests and responses + +### Local Data Signals +Monitors interactions with local data storage. +- `action`: Type of data operation (`loaded`, `updated`, `saved`, `deleted`, `undefined`) +- `identifier`: Identifier for the data being operated on +- `data`: The data being operated on in JSON format (optional) + +### Instrumentation Signals +Captures analytics events from your existing instrumentation. +- `type`: Type of analytics event (`track`, `screen`, `identify`, `group`, `alias`, `unknown`) +- `rawEvent`: The original analytics event data in JSON format + +### User Defined Signals +Create custom signals to capture app-specific events. +- `type`: Always `.userDefined` +- `data`: Custom data structure defined by you + +Example of creating a custom signal: +```swift +struct MyKindaSignal: RawSignal { + struct MyKindaData: Codable { + let that: String + } + + var anonymousId: String = Signals.shared.anonymousId + var type: SignalType = .userDefined + var timestamp: String = Date().iso8601() + var index: Int = Signals.shared.nextIndex + var context: StaticContext? = nil + var data: MyKindaData + + init(that: String) { + self.data = MyKindaData(that: that) + } +} + +... + +// Manually emit the signal in your code +Signals.emit(MyKindaSignal("Rabi Ray Rana")) +``` + +## Signal Broadcasting + +Signals are broadcast according to these rules: + +- `sendDebugSignalsToSegment`: Only broadcasts signals to Segment when explicitly enabled for debugging +- `DebugBroadcaster`: Always broadcasts signals to console, regardless of build configuration +- `WebhookBroadcaster`: Always broadcasts signals to specified webhook URL, regardless of build configuration + +### Custom Broadcasters + +You can create your own broadcasters in two ways: + +1. Conform to `SignalBroadcaster` to work with typed signals: +```swift +public protocol SignalBroadcaster { + var analytics: Analytics? { get set } + func added(signal: any RawSignal) + func relay() +} +``` + +2. Conform to `SignalJSONBroadcaster` to work with raw dictionary data: +```swift +public protocol SignalJSONBroadcaster: SignalBroadcaster { + func added(signal: [String: Any]) +} +``` + +The JSON broadcaster is useful when you need to work with the raw dictionary representation of signals before they're converted to JSON. + +## JavaScript Runtime API + +### Signal Type Constants + +```javascript +const SignalType = { + Interaction: "interaction", + Navigation: "navigation", + Network: "network", + LocalData: "localData", + Instrumentation: "instrumentation", + UserDefined: "userDefined" +} +``` + +### Base Signal Classes + +#### RawSignal +Base class for all signals: + +```javascript +class RawSignal { + constructor(type, data) {} // Create new signal with type and data + + // Properties + anonymousId: string // Anonymous ID from analytics instance + type: SignalType // Type of signal + data: object // Signal-specific data + timestamp: Date // Creation timestamp + index: number // Sequential index (set by signals.add()) + context: object // Static context (set at emit time, optional) +} +``` + +### Navigation Signals + +Navigation signal class: + +```javascript +class NavigationSignal extends RawSignal { + constructor(currentScreen, previousScreen) {} // Create navigation signal + + // Data Properties + data.currentScreen: string // Current screen identifier + data.previousScreen: string // Previous screen identifier (optional) +} +``` + +### Interaction Signals + +```javascript +class InteractionSignal extends RawSignal { + constructor(component, title, data) {} // Create interaction signal + + // Data Properties (nested in target object) + data.target.component: string // UI component type + data.target.title: string // Additional information (optional) + data.target.data: object // Custom interaction data (optional) +} +``` + +### Network Signals + +Network action constants: + +```javascript +const NetworkAction = { + Request: "request", // Outgoing request + Response: "response" // Incoming response +} +``` + +Network signal class: + +```javascript +class NetworkSignal extends RawSignal { + constructor(data) {} // Create network signal with NetworkData + + // Data Properties + data.action: string // NetworkAction value + data.url: string // Request/response URL + data.body: object // Request/response body (optional) + data.contentType: string // Content type (optional) + data.method: string // HTTP method (optional) + data.status: number // HTTP status code (optional) + data.ok: boolean // Success indicator (optional) + data.requestId: string // Unique request identifier +} +``` + +### Local Data Signals + +Local data action constants: + +```javascript +const LocalDataAction = { + Loaded: "loaded", // Data loaded + Updated: "updated", // Data updated + Saved: "saved", // Data saved + Deleted: "deleted", // Data deleted + Undefined: "undefined" // Other operations +} +``` + +Local data signal class: + +```javascript +class LocalDataSignal extends RawSignal { + constructor(action, identifier, data) {} // Create local data signal + + // Data Properties + data.action: string // LocalDataAction value + data.identifier: string // Data identifier + data.data: object // Associated data (optional) +} +``` + +### Instrumentation Signals + +Event type constants: + +```javascript +const EventType = { + Track: "track", // Track events + Screen: "screen", // Screen events + Identify: "identify", // Identify events + Group: "group", // Group events + Alias: "alias", // Alias events + Unknown: "unknown" // Unknown event types +} +``` + +Instrumentation signal class: + +```javascript +class InstrumentationSignal extends RawSignal { + constructor(rawEvent) {} // Create instrumentation signal + + // Data Properties + data.type: string // EventType value + data.rawEvent: object // Original analytics event +} +``` + +### Signals Buffer Management + +The Signals class manages a buffer of recently collected signals and provides methods for searching and processing them: + +```javascript +class Signals { + // Signal Search Methods + find(fromSignal, // Starting signal (optional) + signalType, // Signal type to find (optional) + predicate) {} // Search predicate function + + findAndApply(fromSignal, // Starting signal (optional) + signalType, // Signal type to find (optional) + searchPredicate, // Search criteria + applyPredicate) {} // Function to apply to found signal +} +``` + +A global instance is automatically created and available: + +```javascript +let signals = new Signals() // Global signals buffer instance +``` + +### Usage Examples + +Creating and adding signals: + +```javascript +// Create navigation signal +let navSignal = new NavigationSignal( + "ProductDetail", + "ProductList" +) +signals.add(navSignal) + +// Create interaction signal +let buttonSignal = new InteractionSignal( + "button", + "Add to Cart", + { productId: "123" } +) +signals.add(buttonSignal) +``` + +Finding related signals: + +```javascript +// Find most recent network response +let networkSignal = signals.find( + currentSignal, + SignalType.Network, + (signal) => { + return signal.data.action === NetworkAction.Response + } +) + +// Find and process related signals +signals.findAndApply( + currentSignal, + SignalType.Interaction, + (signal) => signal.data.target.component === "button", + (found) => { + // Process found signal + console.log("Found related interaction:", found) + } +) +``` diff --git a/Sources/AnalyticsLive/LivePlugins/EmbeddedJS.swift b/Sources/AnalyticsLive/LivePlugins/EmbeddedJS.swift index 438c29f..08b9011 100755 --- a/Sources/AnalyticsLive/LivePlugins/EmbeddedJS.swift +++ b/Sources/AnalyticsLive/LivePlugins/EmbeddedJS.swift @@ -16,7 +16,7 @@ public struct EmbeddedJS { before: \(PluginType.before.rawValue), enrichment: \(PluginType.enrichment.rawValue), after: \(PluginType.after.rawValue), - utility: \(PluginType.before.rawValue) + utility: \(PluginType.utility.rawValue) }; """ diff --git a/Sources/AnalyticsLive/LivePlugins/LivePlugin.swift b/Sources/AnalyticsLive/LivePlugins/LivePlugin.swift index 6dfafbe..0d53bf2 100755 --- a/Sources/AnalyticsLive/LivePlugins/LivePlugin.swift +++ b/Sources/AnalyticsLive/LivePlugins/LivePlugin.swift @@ -9,6 +9,17 @@ import Foundation import Substrata import Segment +extension UpdateType { + var stringValue: String { + switch self { + case .initial: + return "Initial" + case .refresh: + return "Refresh" + } + } +} + /** LivePlugin is the wrapper class that will end up calling into the JS for a given LivePlugin. @@ -32,7 +43,7 @@ public class LivePlugin: EventPlugin { public func update(settings: Settings, type: UpdateType) { guard let dict = toDictionary(settings)?.toJSConvertible() else { return } - jsPlugin.call(method: "update", args: [dict]) + jsPlugin.call(method: "update", args: [dict, type.stringValue]) } public func execute(event: T?) -> T? {