@walkeros/server-destination-datamanager 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,666 @@
1
+ # @walkeros/server-destination-datamanager
2
+
3
+ Google Data Manager server destination for walkerOS - send conversion events and
4
+ audience data to Google Ads, Display & Video 360, and Google Analytics 4 through
5
+ a single unified API.
6
+
7
+ ## Features
8
+
9
+ - **Multi-platform reach**: Single API call sends data to Google Ads, DV360, and
10
+ GA4
11
+ - **Privacy-first**: SHA-256 hashing of PII with Gmail-specific email
12
+ normalization
13
+ - **DMA compliance**: Built-in consent management for EEA/UK/Switzerland
14
+ - **Explicit mapping**: Transform walkerOS events to Data Manager format using
15
+ declarative mapping rules
16
+ - **Type-safe**: Full TypeScript support with comprehensive type definitions
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @walkeros/server-destination-datamanager
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### Minimal Configuration
27
+
28
+ ```typescript
29
+ import { destinationDataManager } from '@walkeros/server-destination-datamanager';
30
+ import { startFlow } from '@walkeros/collector';
31
+
32
+ const { collector, elb } = await startFlow({
33
+ destinations: {
34
+ datamanager: {
35
+ ...destinationDataManager,
36
+ config: {
37
+ settings: {
38
+ // OAuth 2.0 access token with datamanager scope
39
+ accessToken: 'ya29.c.xxx',
40
+
41
+ // Destination accounts
42
+ destinations: [
43
+ {
44
+ operatingAccount: {
45
+ accountId: '123-456-7890',
46
+ accountType: 'GOOGLE_ADS',
47
+ },
48
+ productDestinationId: 'AW-CONVERSION-123',
49
+ },
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ },
55
+ });
56
+
57
+ // Track a conversion
58
+ await elb('order complete', {
59
+ id: 'ORDER-123',
60
+ total: 99.99,
61
+ currency: 'USD',
62
+ });
63
+ ```
64
+
65
+ ### Complete Configuration
66
+
67
+ ```typescript
68
+ import { destinationDataManager } from '@walkeros/server-destination-datamanager';
69
+
70
+ const config = {
71
+ ...destinationDataManager,
72
+ config: {
73
+ settings: {
74
+ accessToken: 'ya29.c.xxx',
75
+
76
+ // Multiple destinations (max 10)
77
+ destinations: [
78
+ {
79
+ operatingAccount: {
80
+ accountId: '123-456-7890',
81
+ accountType: 'GOOGLE_ADS',
82
+ },
83
+ productDestinationId: 'AW-CONVERSION-123',
84
+ },
85
+ {
86
+ operatingAccount: {
87
+ accountId: '987654321',
88
+ accountType: 'GOOGLE_ANALYTICS_PROPERTY',
89
+ },
90
+ productDestinationId: 'G-XXXXXXXXXX',
91
+ },
92
+ ],
93
+
94
+ // Optional settings
95
+ eventSource: 'WEB', // Default event source
96
+ batchSize: 100, // Events per batch (max 2000)
97
+ batchInterval: 5000, // Batch flush interval in ms
98
+ validateOnly: false, // Test mode (validate without ingestion)
99
+ testEventCode: 'TEST12345', // For debugging
100
+
101
+ // Request-level consent
102
+ consent: {
103
+ adUserData: 'CONSENT_GRANTED',
104
+ adPersonalization: 'CONSENT_GRANTED',
105
+ },
106
+
107
+ // Guided helpers (apply to all events)
108
+ userData: {
109
+ email: 'user.id',
110
+ phone: 'data.phone',
111
+ },
112
+ userId: 'user.id',
113
+ clientId: 'user.device',
114
+ sessionAttributes: 'context.sessionAttributes',
115
+ },
116
+
117
+ // Event mapping
118
+ mapping: {
119
+ order: {
120
+ complete: {
121
+ name: 'purchase',
122
+ data: {
123
+ map: {
124
+ transactionId: 'data.id',
125
+ conversionValue: 'data.total',
126
+ currency: { key: 'data.currency', value: 'USD' },
127
+ eventName: { value: 'purchase' }, // For GA4
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ },
134
+ };
135
+ ```
136
+
137
+ ## Authentication
138
+
139
+ ### OAuth 2.0 Setup
140
+
141
+ 1. **Create a Google Cloud Project**
142
+ - Go to [Google Cloud Console](https://console.cloud.google.com/)
143
+ - Create a new project or select existing
144
+
145
+ 2. **Enable Data Manager API**
146
+ - Navigate to APIs & Services > Library
147
+ - Search for "Google Data Manager API"
148
+ - Click Enable
149
+
150
+ 3. **Create Service Account**
151
+ - Go to APIs & Services > Credentials
152
+ - Click "Create Credentials" > "Service Account"
153
+ - Grant necessary permissions
154
+ - Download JSON key file
155
+
156
+ 4. **Get Access Token**
157
+ ```bash
158
+ gcloud auth application-default print-access-token --scopes=https://www.googleapis.com/auth/datamanager
159
+ ```
160
+
161
+ ### Required Scope
162
+
163
+ ```
164
+ https://www.googleapis.com/auth/datamanager
165
+ ```
166
+
167
+ ## Guided Mapping Helpers
168
+
169
+ Define common fields once in Settings instead of repeating them in every event
170
+ mapping:
171
+
172
+ ```typescript
173
+ {
174
+ settings: {
175
+ accessToken: 'ya29.c.xxx',
176
+ destinations: [...],
177
+
178
+ // Guided helpers (apply to all events)
179
+ userData: {
180
+ email: 'user.id',
181
+ phone: 'data.phone',
182
+ firstName: 'data.firstName',
183
+ lastName: 'data.lastName',
184
+ },
185
+ userId: 'user.id',
186
+ clientId: 'user.device',
187
+ sessionAttributes: 'context.sessionAttributes',
188
+
189
+ // Consent mapping (string = field name, boolean = static value)
190
+ consentAdUserData: 'marketing', // Read event.consent.marketing
191
+ consentAdPersonalization: 'personalization', // Read event.consent.personalization
192
+ // OR use static values:
193
+ // consentAdUserData: true, // Always CONSENT_GRANTED
194
+ },
195
+ }
196
+ ```
197
+
198
+ **Precedence**: Settings helpers < config.data < event mapping
199
+
200
+ Event mappings always override Settings:
201
+
202
+ ```typescript
203
+ {
204
+ settings: {
205
+ userId: 'user.id', // Default for all events
206
+ },
207
+ mapping: {
208
+ order: {
209
+ complete: {
210
+ data: {
211
+ map: {
212
+ userId: 'data.customerId', // Override for this event
213
+ },
214
+ },
215
+ },
216
+ },
217
+ },
218
+ }
219
+ ```
220
+
221
+ ## Data Mapping
222
+
223
+ ### Event Data Mapping
224
+
225
+ All event data must be explicitly mapped. The destination does not auto-extract
226
+ fields from events.
227
+
228
+ ```typescript
229
+ {
230
+ mapping: {
231
+ order: {
232
+ complete: {
233
+ name: 'purchase',
234
+ data: {
235
+ map: {
236
+ // Transaction data
237
+ transactionId: 'data.id',
238
+ conversionValue: 'data.total',
239
+ currency: { key: 'data.currency', value: 'USD' },
240
+ eventName: { value: 'purchase' },
241
+
242
+ // User identification
243
+ userId: 'user.id',
244
+ email: 'user.id', // Will be SHA-256 hashed
245
+ phone: 'data.phone', // Will be SHA-256 hashed
246
+
247
+ // Attribution identifiers
248
+ gclid: 'context.gclid',
249
+ gbraid: 'context.gbraid',
250
+ },
251
+ },
252
+ },
253
+ },
254
+ },
255
+ }
256
+ ```
257
+
258
+ ### Attribution Identifiers
259
+
260
+ Attribution identifiers must be explicitly mapped:
261
+
262
+ ```typescript
263
+ {
264
+ mapping: {
265
+ order: {
266
+ complete: {
267
+ data: {
268
+ map: {
269
+ gclid: 'context.gclid', // From URL: ?gclid=TeSter
270
+ gbraid: 'context.gbraid', // iOS attribution
271
+ wbraid: 'context.wbraid', // Web-to-app
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ }
278
+ ```
279
+
280
+ ### Consent Mapping
281
+
282
+ Map your consent field names to Data Manager's required fields:
283
+
284
+ ```typescript
285
+ {
286
+ settings: {
287
+ // Map from your consent field names
288
+ consentAdUserData: 'marketing', // Read event.consent.marketing
289
+ consentAdPersonalization: 'personalization', // Read event.consent.personalization
290
+ },
291
+ }
292
+
293
+ // Your event with standard consent field names
294
+ await elb('order complete', { total: 99.99 }, {
295
+ consent: {
296
+ marketing: true,
297
+ personalization: false,
298
+ },
299
+ });
300
+
301
+ // Becomes Data Manager format
302
+ {
303
+ consent: {
304
+ adUserData: 'CONSENT_GRANTED',
305
+ adPersonalization: 'CONSENT_DENIED'
306
+ }
307
+ }
308
+ ```
309
+
310
+ **Static values** for always-on consent:
311
+
312
+ ```typescript
313
+ {
314
+ settings: {
315
+ consentAdUserData: true, // Always CONSENT_GRANTED
316
+ consentAdPersonalization: false, // Always CONSENT_DENIED
317
+ },
318
+ }
319
+ ```
320
+
321
+ **Fallback**: Without consent mapping, uses `event.consent.marketing` →
322
+ `adUserData` and `event.consent.personalization` → `adPersonalization`.
323
+
324
+ ## Event Mapping Examples
325
+
326
+ ### E-commerce Purchase
327
+
328
+ ```typescript
329
+ {
330
+ mapping: {
331
+ order: {
332
+ complete: {
333
+ name: 'purchase',
334
+ data: {
335
+ map: {
336
+ transactionId: 'data.id',
337
+ conversionValue: 'data.total',
338
+ currency: { key: 'data.currency', value: 'USD' },
339
+ eventName: { value: 'purchase' },
340
+
341
+ // Map nested products to cart items
342
+ cartData: {
343
+ map: {
344
+ items: {
345
+ loop: [
346
+ 'nested',
347
+ {
348
+ condition: (entity) => entity.entity === 'product',
349
+ map: {
350
+ merchantProductId: 'data.id',
351
+ price: 'data.price',
352
+ quantity: { key: 'data.quantity', value: 1 },
353
+ },
354
+ },
355
+ ],
356
+ },
357
+ },
358
+ },
359
+ },
360
+ },
361
+ },
362
+ },
363
+ },
364
+ }
365
+ ```
366
+
367
+ ### Lead Generation
368
+
369
+ ```typescript
370
+ {
371
+ mapping: {
372
+ lead: {
373
+ submit: {
374
+ name: 'generate_lead',
375
+ data: {
376
+ map: {
377
+ eventName: { value: 'generate_lead' },
378
+ conversionValue: { value: 10 },
379
+ currency: { value: 'USD' },
380
+ },
381
+ },
382
+ },
383
+ },
384
+ },
385
+ }
386
+ ```
387
+
388
+ ### Page View (GA4)
389
+
390
+ ```typescript
391
+ {
392
+ mapping: {
393
+ page: {
394
+ view: {
395
+ name: 'page_view',
396
+ data: {
397
+ map: {
398
+ eventName: { value: 'page_view' },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ }
405
+ ```
406
+
407
+ ## Account Types
408
+
409
+ ### Google Ads
410
+
411
+ ```typescript
412
+ {
413
+ operatingAccount: {
414
+ accountId: '123-456-7890', // Format: XXX-XXX-XXXX
415
+ accountType: 'GOOGLE_ADS',
416
+ },
417
+ productDestinationId: 'AW-CONVERSION-123', // Conversion action ID
418
+ }
419
+ ```
420
+
421
+ ### Display & Video 360
422
+
423
+ ```typescript
424
+ {
425
+ operatingAccount: {
426
+ accountId: '12345', // Advertiser ID
427
+ accountType: 'DISPLAY_VIDEO_ADVERTISER',
428
+ },
429
+ productDestinationId: 'FL-ACTIVITY-123', // Floodlight activity ID
430
+ }
431
+ ```
432
+
433
+ ### Google Analytics 4
434
+
435
+ ```typescript
436
+ {
437
+ operatingAccount: {
438
+ accountId: '123456789', // Property ID
439
+ accountType: 'GOOGLE_ANALYTICS_PROPERTY',
440
+ },
441
+ productDestinationId: 'G-XXXXXXXXXX', // Measurement ID
442
+ }
443
+ ```
444
+
445
+ ## Data Formatting
446
+
447
+ ### Email Normalization
448
+
449
+ - Trim whitespace
450
+ - Convert to lowercase
451
+ - Remove dots (.) for gmail.com/googlemail.com
452
+ - SHA-256 hash
453
+
454
+ **Example:**
455
+
456
+ ```
457
+ Input: John.Doe@Gmail.com
458
+ Output: <SHA-256 hash of "johndoe@gmail.com">
459
+ ```
460
+
461
+ ### Phone Normalization
462
+
463
+ - Convert to E.164 format: `+[country][number]`
464
+ - Remove all non-digit characters except leading +
465
+ - SHA-256 hash
466
+
467
+ **Example:**
468
+
469
+ ```
470
+ Input: (800) 555-0100
471
+ Output: <SHA-256 hash of "+18005550100">
472
+ ```
473
+
474
+ ### Address Formatting
475
+
476
+ - **Names**: Lowercase, remove titles/suffixes, SHA-256 hash
477
+ - **Region Code**: ISO-3166-1 alpha-2, NOT hashed (e.g., "US")
478
+ - **Postal Code**: NOT hashed
479
+
480
+ ## Consent Management (DMA)
481
+
482
+ ### Required for EEA/UK/Switzerland
483
+
484
+ ```typescript
485
+ // Event-level consent
486
+ await elb('order complete', {
487
+ total: 99.99,
488
+ }, {
489
+ consent: {
490
+ marketing: true,
491
+ personalization: false,
492
+ },
493
+ });
494
+
495
+ // Request-level consent (applies to all events)
496
+ {
497
+ settings: {
498
+ consent: {
499
+ adUserData: 'CONSENT_GRANTED',
500
+ adPersonalization: 'CONSENT_GRANTED',
501
+ },
502
+ },
503
+ }
504
+ ```
505
+
506
+ ## Testing
507
+
508
+ ### Validate Mode
509
+
510
+ Test requests without actually ingesting data:
511
+
512
+ ```typescript
513
+ {
514
+ settings: {
515
+ validateOnly: true, // Validates structure without ingestion
516
+ testEventCode: 'TEST12345', // For debugging
517
+ },
518
+ }
519
+ ```
520
+
521
+ ### Test Event Code
522
+
523
+ Add test event code for debugging in production:
524
+
525
+ ```typescript
526
+ {
527
+ settings: {
528
+ testEventCode: 'TEST12345',
529
+ },
530
+ }
531
+ ```
532
+
533
+ ## Debug Mode
534
+
535
+ Enable debug logging to see API requests and responses:
536
+
537
+ ```typescript
538
+ {
539
+ settings: {
540
+ logLevel: 'debug', // Shows all API calls and responses
541
+ },
542
+ }
543
+ ```
544
+
545
+ **Log levels**: `debug` (all), `info`, `warn`, `error`, `none` (default).
546
+
547
+ Debug mode logs:
548
+
549
+ - Event processing details
550
+ - API request payload and destination count
551
+ - API response status and request ID
552
+ - Validation errors with full context
553
+
554
+ ## Deduplication with gtag
555
+
556
+ Prevent double-counting between client-side gtag and server-side Data Manager:
557
+
558
+ ### Transaction ID Matching
559
+
560
+ Use the same transaction ID across both platforms:
561
+
562
+ ```javascript
563
+ // Client-side gtag
564
+ gtag('event', 'conversion', {
565
+ transaction_id: 'ORDER-123',
566
+ send_to: 'AW-CONVERSION/xxx',
567
+ });
568
+ ```
569
+
570
+ ```typescript
571
+ // Server-side walkerOS
572
+ await elb('order complete', {
573
+ id: 'ORDER-123', // Must map to transactionId via mapping config
574
+ });
575
+ ```
576
+
577
+ Google deduplicates using `transactionId` within 14 days. You must explicitly
578
+ map the transaction ID in your configuration.
579
+
580
+ ### GCLID Attribution
581
+
582
+ Map GCLID from your event structure:
583
+
584
+ ```typescript
585
+ {
586
+ mapping: {
587
+ order: {
588
+ complete: {
589
+ data: {
590
+ map: {
591
+ gclid: 'context.gclid', // From URL parameter
592
+ transactionId: 'data.id',
593
+ },
594
+ },
595
+ },
596
+ },
597
+ },
598
+ }
599
+ ```
600
+
601
+ GCLID is captured by walkerOS browser source from URL parameters (`?gclid=xxx`)
602
+ and stored in `context.gclid`.
603
+
604
+ ## Rate Limits
605
+
606
+ - **300 requests per minute** per project
607
+ - **100,000 requests per day** per project
608
+ - Error code: `RESOURCE_EXHAUSTED` (HTTP 429)
609
+
610
+ ## Conversion Window
611
+
612
+ **CRITICAL**: Events must occur within the last **14 days**. Older events will
613
+ be rejected.
614
+
615
+ ## API Reference
616
+
617
+ ### Settings
618
+
619
+ | Property | Type | Required | Description |
620
+ | -------------------------- | -------------- | -------- | ------------------------------------------- |
621
+ | `accessToken` | string | ✓ | OAuth 2.0 access token |
622
+ | `destinations` | Destination[] | ✓ | Array of destination accounts (max 10) |
623
+ | `eventSource` | EventSource | | Default event source (WEB, APP, etc.) |
624
+ | `batchSize` | number | | Max events per batch (max 2000) |
625
+ | `batchInterval` | number | | Batch flush interval in ms |
626
+ | `validateOnly` | boolean | | Validate without ingestion |
627
+ | `url` | string | | Override API endpoint |
628
+ | `consent` | Consent | | Request-level consent |
629
+ | `testEventCode` | string | | Test event code for debugging |
630
+ | `userData` | object | | Guided helper: User data mapping |
631
+ | `userId` | string | | Guided helper: First-party user ID |
632
+ | `clientId` | string | | Guided helper: GA4 client ID |
633
+ | `sessionAttributes` | string | | Guided helper: Privacy-safe attribution |
634
+ | `consentAdUserData` | string/boolean | | Consent mapping: Field name or static value |
635
+ | `consentAdPersonalization` | string/boolean | | Consent mapping: Field name or static value |
636
+
637
+ ### Event Fields
638
+
639
+ | Field | Type | Max Length | Description |
640
+ | ----------------- | ------ | ---------- | -------------------------------- |
641
+ | `transactionId` | string | 512 | Transaction ID for deduplication |
642
+ | `clientId` | string | 255 | GA client ID |
643
+ | `userId` | string | 256 | First-party user ID |
644
+ | `conversionValue` | number | | Conversion value |
645
+ | `currency` | string | 3 | ISO 4217 currency code |
646
+ | `eventName` | string | 40 | Event name (required for GA4) |
647
+ | `eventSource` | string | | WEB, APP, IN_STORE, PHONE, OTHER |
648
+
649
+ ## Resources
650
+
651
+ - [Google Data Manager API Documentation](https://developers.google.com/data-manager/api)
652
+ - [Data Formatting Guidelines](https://developers.google.com/data-manager/api/devguides/concepts/formatting)
653
+ - [DMA Compliance](https://developers.google.com/data-manager/api/devguides/concepts/dma)
654
+ - [walkerOS Documentation](https://www.elbwalker.com/docs/)
655
+
656
+ ## License
657
+
658
+ MIT
659
+
660
+ ## Support
661
+
662
+ For issues and questions:
663
+
664
+ - [GitHub Issues](https://github.com/elbwalker/walkerOS/issues)
665
+ - [walkerOS Documentation](https://www.elbwalker.com/docs/)
666
+ - [Google Data Manager Support](https://developers.google.com/data-manager/api/support/contact)