|
| 1 | +# Snowplow Unity Tracker - Claude Code Documentation |
| 2 | + |
| 3 | +## Project Overview |
| 4 | +The Snowplow Unity Tracker is a C# library for Unity that enables event tracking and analytics in Unity games and applications. It sends events to Snowplow collectors for data analysis, supporting multiple platforms including iOS, Android, Windows, Mac, Linux, and WebGL. |
| 5 | + |
| 6 | +## Development Commands |
| 7 | +```bash |
| 8 | +# Build the tracker library |
| 9 | +cd SnowplowTracker && dotnet build |
| 10 | + |
| 11 | +# Run tests (requires Unity 2021.3.12f1 LTS) |
| 12 | +# Open SnowplowTracker.Tests in Unity Editor |
| 13 | +# Window -> General -> Test Runner -> EditMode -> Run All |
| 14 | + |
| 15 | +# Start test collector (requires Vagrant) |
| 16 | +vagrant up && vagrant ssh |
| 17 | +cd /vagrant && mb & |
| 18 | +curl -X POST -d @/vagrant/Resources/imposter.json http://localhost:2525/imposters |
| 19 | + |
| 20 | +# Build demo game |
| 21 | +# Open SnowplowTracker.Demo in Unity Editor and build |
| 22 | +``` |
| 23 | + |
| 24 | +## Architecture |
| 25 | +The tracker follows a layered architecture with clear separation of concerns: |
| 26 | + |
| 27 | +### Core Components |
| 28 | +- **Tracker**: Main entry point for event tracking operations |
| 29 | +- **Emitter**: Handles event transmission (AsyncEmitter, SyncEmitter, WebGlEmitter) |
| 30 | +- **Storage**: Event persistence using LiteDB or in-memory storage |
| 31 | +- **Events**: Strongly-typed event classes with builder pattern |
| 32 | +- **Payloads**: Event data structures and JSON serialization |
| 33 | +- **Subject**: User/device information management |
| 34 | +- **Session**: Session state and management |
| 35 | + |
| 36 | +### Data Flow |
| 37 | +1. Application creates events using builder pattern |
| 38 | +2. Tracker validates and enriches events with context |
| 39 | +3. Events are queued in storage (LiteDB or memory) |
| 40 | +4. Emitter batches and sends events to collector |
| 41 | +5. Failed events are retried with backoff |
| 42 | + |
| 43 | +## Core Architectural Principles |
| 44 | + |
| 45 | +### 1. Builder Pattern for Events |
| 46 | +All events use fluent builder pattern for construction: |
| 47 | +```csharp |
| 48 | +// ✅ Correct: Use builder pattern |
| 49 | +var event = new PageView() |
| 50 | + .SetPageUrl("https://example.com") |
| 51 | + .SetCustomContext(contexts) |
| 52 | + .Build(); |
| 53 | +``` |
| 54 | + |
| 55 | +### 2. Async-First Design |
| 56 | +Primary emitter is asynchronous with thread-safe operations: |
| 57 | +```csharp |
| 58 | +// ✅ Correct: Use AsyncEmitter for production |
| 59 | +IEmitter emitter = new AsyncEmitter(collectorUrl, |
| 60 | + HttpProtocol.HTTPS, HttpMethod.POST); |
| 61 | +``` |
| 62 | + |
| 63 | +### 3. Platform-Specific Adaptations |
| 64 | +Different implementations for different Unity platforms: |
| 65 | +```csharp |
| 66 | +// ✅ Correct: Platform-specific emitter selection |
| 67 | +IEmitter emitter = Application.platform == RuntimePlatform.WebGLPlayer |
| 68 | + ? new WebGlEmitter(url) |
| 69 | + : new AsyncEmitter(url); |
| 70 | +``` |
| 71 | + |
| 72 | +### 4. Immutable Event Data |
| 73 | +Events are immutable once built to ensure thread safety: |
| 74 | +```csharp |
| 75 | +// ❌ Wrong: Modifying built event |
| 76 | +var event = new Structured().Build(); |
| 77 | +event.category = "new"; // Compilation error |
| 78 | +
|
| 79 | +// ✅ Correct: Configure before building |
| 80 | +var event = new Structured() |
| 81 | + .SetCategory("category") |
| 82 | + .Build(); |
| 83 | +``` |
| 84 | + |
| 85 | +## Layer Organization & Responsibilities |
| 86 | + |
| 87 | +### SnowplowTracker/ (Core Library) |
| 88 | +- **Root**: Core tracker, subject, session, utilities |
| 89 | +- **Emitters/**: Event transmission strategies |
| 90 | +- **Events/**: Event type definitions and builders |
| 91 | +- **Payloads/**: Data structures and serialization |
| 92 | +- **Storage/**: Event persistence implementations |
| 93 | +- **Requests/**: HTTP request handling |
| 94 | +- **Enums/**: Type-safe enumerations |
| 95 | + |
| 96 | +### SnowplowTracker.Tests/ (Unit Tests) |
| 97 | +- **Tests/**: NUnit test fixtures |
| 98 | +- **TestHelpers/**: Mock implementations and utilities |
| 99 | + |
| 100 | +### SnowplowTracker.Demo/ (Example Unity Game) |
| 101 | +- **Scripts/**: Demo game implementation with tracker usage |
| 102 | + |
| 103 | +## Critical Import Patterns |
| 104 | + |
| 105 | +### Namespace Organization |
| 106 | +```csharp |
| 107 | +// ✅ Correct: Organized namespace imports |
| 108 | +using SnowplowTracker; |
| 109 | +using SnowplowTracker.Emitters; |
| 110 | +using SnowplowTracker.Events; |
| 111 | +using SnowplowTracker.Payloads.Contexts; |
| 112 | +using SnowplowTracker.Enums; |
| 113 | +``` |
| 114 | + |
| 115 | +### Unity-Specific Imports |
| 116 | +```csharp |
| 117 | +// ✅ Correct: Unity engine imports when needed |
| 118 | +using UnityEngine; |
| 119 | +using System.Collections; |
| 120 | +``` |
| 121 | + |
| 122 | +## Essential Library Patterns |
| 123 | + |
| 124 | +### Tracker Initialization |
| 125 | +```csharp |
| 126 | +// ✅ Correct: Full initialization with session |
| 127 | +var emitter = new AsyncEmitter(collectorUrl); |
| 128 | +var subject = new Subject().SetUserId(userId); |
| 129 | +var session = new Session(null); |
| 130 | +var tracker = new Tracker(emitter, "namespace", |
| 131 | + "appId", subject, session); |
| 132 | +tracker.StartEventTracking(); |
| 133 | +``` |
| 134 | + |
| 135 | +### Event Tracking |
| 136 | +```csharp |
| 137 | +// ✅ Correct: Track with contexts |
| 138 | +tracker.Track(new Structured() |
| 139 | + .SetCategory("game") |
| 140 | + .SetAction("click") |
| 141 | + .SetCustomContext(contexts) |
| 142 | + .Build()); |
| 143 | +``` |
| 144 | + |
| 145 | +### Context Creation |
| 146 | +```csharp |
| 147 | +// ✅ Correct: Build contexts with fluent API |
| 148 | +var context = new MobileContext() |
| 149 | + .SetOsType("iOS") |
| 150 | + .SetOsVersion("14.0") |
| 151 | + .SetDeviceModel("iPhone 12") |
| 152 | + .Build(); |
| 153 | +``` |
| 154 | + |
| 155 | +## Model Organization Pattern |
| 156 | + |
| 157 | +### Event Hierarchy |
| 158 | +- **IEvent**: Base interface for all events |
| 159 | +- **AbstractEvent<T>**: Generic base with common fields |
| 160 | +- **Concrete Events**: PageView, Structured, Unstructured, etc. |
| 161 | + |
| 162 | +### Payload Structure |
| 163 | +- **IPayload**: Base payload interface |
| 164 | +- **TrackerPayload**: Key-value event data |
| 165 | +- **SelfDescribingJson**: Schema-based JSON payloads |
| 166 | + |
| 167 | +### Context Types |
| 168 | +- **IContext**: Base context interface |
| 169 | +- **AbstractContext**: Common context implementation |
| 170 | +- **Specific Contexts**: Mobile, Desktop, GeoLocation, Session |
| 171 | + |
| 172 | +## Common Pitfalls & Solutions |
| 173 | + |
| 174 | +### Storage on Mobile Platforms |
| 175 | +```csharp |
| 176 | +// ❌ Wrong: Using file storage on tvOS |
| 177 | +IStore store = new EventStore(); // Fails on tvOS |
| 178 | +
|
| 179 | +// ✅ Correct: Platform-appropriate storage |
| 180 | +IStore store = Application.platform == RuntimePlatform.tvOS |
| 181 | + ? new InMemoryEventStore() |
| 182 | + : new EventStore(); |
| 183 | +``` |
| 184 | + |
| 185 | +### Thread Safety in Emitters |
| 186 | +```csharp |
| 187 | +// ❌ Wrong: Direct collection manipulation |
| 188 | +emitter.eventQueue.Add(event); // Not thread-safe |
| 189 | +
|
| 190 | +// ✅ Correct: Use thread-safe Add method |
| 191 | +emitter.Add(payload); // Thread-safe |
| 192 | +``` |
| 193 | + |
| 194 | +### Base64 Encoding for Custom Events |
| 195 | +```csharp |
| 196 | +// ❌ Wrong: Forgetting base64 encoding |
| 197 | +var tracker = new Tracker(emitter, ns, appId, |
| 198 | + base64Encoded: false); // May cause issues |
| 199 | +
|
| 200 | +// ✅ Correct: Enable base64 for compatibility |
| 201 | +var tracker = new Tracker(emitter, ns, appId, |
| 202 | + base64Encoded: true); // Default and recommended |
| 203 | +``` |
| 204 | + |
| 205 | +### Unity Lifecycle Management |
| 206 | +```csharp |
| 207 | +// ❌ Wrong: Not stopping tracker |
| 208 | +void OnApplicationPause(bool pause) { } |
| 209 | + |
| 210 | +// ✅ Correct: Manage tracker lifecycle |
| 211 | +void OnApplicationPause(bool pause) { |
| 212 | + if (pause) tracker.StopEventTracking(); |
| 213 | + else tracker.StartEventTracking(); |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +## File Structure Template |
| 218 | +``` |
| 219 | +snowplow-unity-tracker/ |
| 220 | +├── SnowplowTracker/ # Core library project |
| 221 | +│ └── SnowplowTracker/ |
| 222 | +│ ├── Emitters/ # Event transmission |
| 223 | +│ ├── Events/ # Event types |
| 224 | +│ ├── Payloads/ # Data structures |
| 225 | +│ │ └── Contexts/ # Context types |
| 226 | +│ ├── Storage/ # Persistence layer |
| 227 | +│ ├── Requests/ # HTTP handling |
| 228 | +│ ├── Enums/ # Type enumerations |
| 229 | +│ ├── Tracker.cs # Main tracker class |
| 230 | +│ ├── Subject.cs # User/device info |
| 231 | +│ └── Session.cs # Session management |
| 232 | +├── SnowplowTracker.Tests/ # Unit test project |
| 233 | +│ └── Assets/Tests/ # NUnit test fixtures |
| 234 | +└── SnowplowTracker.Demo/ # Demo Unity game |
| 235 | + └── Assets/Scripts/ # Example implementation |
| 236 | +``` |
| 237 | + |
| 238 | +## Quick Reference |
| 239 | + |
| 240 | +### Import Checklist |
| 241 | +- [ ] `using SnowplowTracker;` - Core namespace |
| 242 | +- [ ] `using SnowplowTracker.Emitters;` - For emitter types |
| 243 | +- [ ] `using SnowplowTracker.Events;` - For event builders |
| 244 | +- [ ] `using SnowplowTracker.Payloads.Contexts;` - For contexts |
| 245 | +- [ ] `using SnowplowTracker.Enums;` - For enumerations |
| 246 | + |
| 247 | +### Common Event Types |
| 248 | +- **PageView**: Web page views in WebGL builds |
| 249 | +- **ScreenView**: Mobile screen transitions |
| 250 | +- **Structured**: Custom categorized events |
| 251 | +- **Unstructured**: Schema-based custom events |
| 252 | +- **Timing**: Performance measurements |
| 253 | +- **EcommerceTransaction**: Purchase events |
| 254 | + |
| 255 | +### Platform Considerations |
| 256 | +- **iOS/Android**: Use AsyncEmitter with EventStore |
| 257 | +- **WebGL**: Use WebGlEmitter for browser compatibility |
| 258 | +- **tvOS**: Use InMemoryEventStore (no file system) |
| 259 | +- **Desktop**: Full feature support with all emitters |
| 260 | + |
| 261 | +### Testing Patterns |
| 262 | +- Use `BaseEmitter` for mocking in tests |
| 263 | +- Test with local Mountebank collector |
| 264 | +- Verify event payloads with `GetLastEvent()` |
| 265 | +- Check thread safety with concurrent operations |
| 266 | + |
| 267 | +## Contributing to CLAUDE.md |
| 268 | + |
| 269 | +When adding or updating content in this document, please follow these guidelines: |
| 270 | + |
| 271 | +### File Size Limit |
| 272 | +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) |
| 273 | +- Check file size after updates: `wc -c CLAUDE.md` |
| 274 | +- Remove outdated content if approaching the limit |
| 275 | + |
| 276 | +### Code Examples |
| 277 | +- Keep all code examples **4 lines or fewer** |
| 278 | +- Focus on the essential pattern, not complete implementations |
| 279 | +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches |
| 280 | + |
| 281 | +### Content Organization |
| 282 | +- Add new patterns to existing sections when possible |
| 283 | +- Create new sections sparingly to maintain structure |
| 284 | +- Update the architectural principles section for major changes |
| 285 | +- Ensure examples follow current codebase conventions |
| 286 | + |
| 287 | +### Quality Standards |
| 288 | +- Test any new patterns in actual code before documenting |
| 289 | +- Verify imports and syntax are correct for the codebase |
| 290 | +- Keep language concise and actionable |
| 291 | +- Focus on "what" and "how", minimize "why" explanations |
| 292 | + |
| 293 | +### Multiple CLAUDE.md Files |
| 294 | +- **Directory-specific CLAUDE.md files** can be created for specialized modules |
| 295 | +- Follow the same structure and guidelines as this root CLAUDE.md |
| 296 | +- Keep them focused on directory-specific patterns and conventions |
| 297 | +- Maximum 20KB per directory-specific CLAUDE.md file |
| 298 | + |
| 299 | +### Instructions for LLMs |
| 300 | +When editing files in this repository, **always check for CLAUDE.md guidance**: |
| 301 | + |
| 302 | +1. **Look for CLAUDE.md in the same directory** as the file being edited |
| 303 | +2. **If not found, check parent directories** recursively up to project root |
| 304 | +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md |
| 305 | +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist |
0 commit comments