From 2c56c71c4ea90ce8307c3b30ab49858c43240b3f Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Mon, 21 Jul 2025 13:42:53 +0800 Subject: [PATCH 1/9] Integration Sample for Identity Map V3 --- .../.gitignore | 18 ++ .../.python-version | 1 + .../README.md | 94 ++++++++ .../attribution_analysis.py | 59 +++++ .../complete_demo.py | 45 ++++ .../config.py | 39 +++ .../database.py | 107 +++++++++ .../env.example | 3 + .../map_identities.py | 225 ++++++++++++++++++ .../populate_test_conversions_impressions.py | 114 +++++++++ .../populate_test_uid_mappings.py | 183 ++++++++++++++ .../pyproject.toml | 14 ++ .../uid_client_wrapper.py | 52 ++++ 13 files changed, 954 insertions(+) create mode 100644 static/examples/identity-map-integration-example/.gitignore create mode 100644 static/examples/identity-map-integration-example/.python-version create mode 100644 static/examples/identity-map-integration-example/README.md create mode 100644 static/examples/identity-map-integration-example/attribution_analysis.py create mode 100644 static/examples/identity-map-integration-example/complete_demo.py create mode 100644 static/examples/identity-map-integration-example/config.py create mode 100644 static/examples/identity-map-integration-example/database.py create mode 100644 static/examples/identity-map-integration-example/env.example create mode 100644 static/examples/identity-map-integration-example/map_identities.py create mode 100644 static/examples/identity-map-integration-example/populate_test_conversions_impressions.py create mode 100644 static/examples/identity-map-integration-example/populate_test_uid_mappings.py create mode 100644 static/examples/identity-map-integration-example/pyproject.toml create mode 100644 static/examples/identity-map-integration-example/uid_client_wrapper.py diff --git a/static/examples/identity-map-integration-example/.gitignore b/static/examples/identity-map-integration-example/.gitignore new file mode 100644 index 000000000..24c6b4c24 --- /dev/null +++ b/static/examples/identity-map-integration-example/.gitignore @@ -0,0 +1,18 @@ +# Environment variables +.env + +# Python cache +__pycache__/ +*.pyc + +# Virtual environments (legacy and uv) +venv/ +.venv/ + +# uv lock file +uv.lock + +# Database files +*.db + +.idea \ No newline at end of file diff --git a/static/examples/identity-map-integration-example/.python-version b/static/examples/identity-map-integration-example/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/static/examples/identity-map-integration-example/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/static/examples/identity-map-integration-example/README.md b/static/examples/identity-map-integration-example/README.md new file mode 100644 index 000000000..24ab4646a --- /dev/null +++ b/static/examples/identity-map-integration-example/README.md @@ -0,0 +1,94 @@ +# UID2 Integration Technical Sample + +**Complete UID2 integration example demonstrating Identity Map V3 flow.** + +This sample shows a pattern for mapping email addresses and phone numbers to UID tokens, handling optouts, managing token refresh cycles, and performing a sample attribution analysis based on both current and previous UIDs. + +## Quick Start + +### 1. Install Dependencies +```bash +# Install uv (Python package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install project dependencies +uv sync +``` + +### 2. Configure UID2 Credentials +```bash +cp .env.example .env +# Edit .env with your UID2 integration credentials +``` + +Required `.env` format: +``` +UID2_BASE_URL=operator-integ.uidapi.com +UID2_API_KEY=your_api_key_here +UID2_SECRET_KEY=your_secret_key_here +``` + +### 3. Run Complete Demo +```bash +# Full workflow: test data → UID2 mapping → attribution analysis +uv run complete_demo.py +``` + +## Core UID2 Integration Patterns + +### Identity Mapping Workflow + +**Key Integration Points:** +1. **Batch Processing** (`map_identities.py:build_uid2_input()`) - Process sequential batches of up to 5,000 DIIs per request +2. **Retry Logic** (`uid_client_wrapper.py:generate_identity_map_with_retry()`) - Exponential backoff for network resilience +3. **Response Handling** (`map_identities.py:process_uid2_response()`) - Process mapped, opted-out, and invalid identifiers + +## Sample Database Schema + +**Core `uid_mapping` table:** +```sql +CREATE TABLE uid_mapping ( + uid_mapping_id INTEGER PRIMARY KEY, + dii TEXT NOT NULL, -- Email or phone (+E.164) + dii_type TEXT NOT NULL, -- 'email' or 'phone' + current_uid TEXT, -- Current UID2 token + previous_uid TEXT, -- Previous UID2 token (for attribution) + refresh_from TIMESTAMP, -- When to refresh mapping + opt_out BOOLEAN DEFAULT FALSE -- Permanent optout flag +); +``` + +**Key business logic queries:** +```sql +-- Records needing mapping (never mapped + refresh expired) +SELECT uid_mapping_id, dii, dii_type +FROM uid_mapping +WHERE opt_out = FALSE +AND (current_uid IS NULL OR refresh_from < datetime('now')); + +-- Attribution joins using both current and previous UIDs +SELECT * FROM impressions imp +JOIN uid_mapping um ON (imp.uid2 = um.current_uid OR imp.uid2 = um.previous_uid) +WHERE um.opt_out = FALSE; +``` + +## Script Reference + +| Script | Purpose | Key Integration Concepts | +|--------|---------|-------------------------| +| `populate_test_uid_mappings.py` | Creates 100k test records | Database schema, DII formatting | +| `map_identities.py` | **Core UID2 mapping logic** | Batch processing, retry logic, response handling | +| `populate_test_conversions_impressions.py` | Attribution demo data | UID2 token usage in measurement | +| `attribution_analysis.py` | Attribution analysis | Cross-UID joins, measurement patterns | +| `complete_demo.py` | End-to-end workflow | Full integration validation | + +## Production Integration Checklist + +**Critical Requirements for UID2 Integration:** + +✅ **Request Limits**: Maximum 5,000 DIIs per request +✅ **Sequential Processing**: No parallel requests to UID2 service +✅ **Retry Logic**: Exponential backoff for network failures +✅ **Optout Handling**: Permanent exclusion from future processing +✅ **Token Refresh**: Respect `refresh_from` timestamps +✅ **State Persistence**: Track mapping state between runs diff --git a/static/examples/identity-map-integration-example/attribution_analysis.py b/static/examples/identity-map-integration-example/attribution_analysis.py new file mode 100644 index 000000000..1841da633 --- /dev/null +++ b/static/examples/identity-map-integration-example/attribution_analysis.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Simple demo of joining impression and conversion data via current and previous UIDs +""" +import sqlite3 +import traceback +from database import get_connection + + +def attribution_analysis(conn: sqlite3.Connection) -> None: + """Run simple attribution analysis query""" + cursor = conn.cursor() + + attribution_query = """ + SELECT + imp.impression_id, + conv.conversion_id, + conv.conversion_value, + imp.campaign_id, + um.dii, + um.current_uid + FROM impressions imp + JOIN uid_mapping um ON (imp.uid = um.current_uid OR imp.uid = um.previous_uid) + JOIN conversions conv ON (conv.uid = um.current_uid OR conv.uid = um.previous_uid) + WHERE um.opt_out = FALSE + ORDER BY RANDOM() + LIMIT 10 + """ + + cursor.execute(attribution_query) + results = cursor.fetchall() + + print("Sample Attribution Results:") + print( + f"{'Impression':<12} {'Conversion':<12} {'Value':<10} {'Campaign':<12} {'DII':<40} {'UID':<15}" + ) + print("-" * 110) + + for row in results: + imp_id, conv_id, value, campaign, dii, uid = row + print( + f"{imp_id:<12} {conv_id:<12} ${value:<9.2f} {campaign:<12} {dii:<40} {uid:<15}" + ) + + +def main(): + try: + conn = get_connection() + attribution_analysis(conn) + except Exception as e: + print(f"Attribution analysis failed: {e}") + traceback.print_exc() + finally: + if "conn" in locals(): + conn.close() + + +if __name__ == "__main__": + main() diff --git a/static/examples/identity-map-integration-example/complete_demo.py b/static/examples/identity-map-integration-example/complete_demo.py new file mode 100644 index 000000000..3f63ff7d0 --- /dev/null +++ b/static/examples/identity-map-integration-example/complete_demo.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +""" +Complete demo of UID Identity Mapping: + - Creates a test database. + - Populates the database with test identity mapping data. + - Runs the UID mapping process. + - Populates the database with test impression and conversion data. + - Runs a sample attribution analysis. +""" +import traceback + +import map_identities +import populate_test_uid_mappings +import populate_test_conversions_impressions +import attribution_analysis +from database import get_connection + + +def complete_demo(): + conn = get_connection("uid_demo.db") + try: + print("Step 1: Populating UID mapping test data...") + populate_test_uid_mappings.populate_database(conn) + + print("Step 2: Running UID mapping...") + map_identities.map_identities(conn) + + print("Step 3: Populating attribution test data...") + populate_test_conversions_impressions.populate_attribution_data(conn) + + print("Step 4: Running attribution analysis...") + attribution_analysis.attribution_analysis(conn) + + print("Demo completed successfully!") + + except Exception as e: + print(f"Failed with error: {e}") + traceback.print_exc() + + finally: + conn.close() + + +if __name__ == "__main__": + complete_demo() diff --git a/static/examples/identity-map-integration-example/config.py b/static/examples/identity-map-integration-example/config.py new file mode 100644 index 000000000..936a876a4 --- /dev/null +++ b/static/examples/identity-map-integration-example/config.py @@ -0,0 +1,39 @@ +import os +import sys +from dataclasses import dataclass + +from dotenv import load_dotenv + + +@dataclass +class Config: + uid_base_url: str + uid_api_key: str + uid_secret_key: str + + +def load_config() -> Config: + load_dotenv(override=True) # Override existing environment variables + + uid_base_url = os.getenv("UID2_BASE_URL") + uid_api_key = os.getenv("UID2_API_KEY") + uid_secret_key = os.getenv("UID2_SECRET_KEY") + + missing: list[str] = [] + if not uid_base_url: + missing.append("UID2_BASE_URL") + if not uid_api_key: + missing.append("UID2_API_KEY") + if not uid_secret_key: + missing.append("UID2_SECRET_KEY") + + if missing: + print(f"Error: Missing required environment variables: {missing}") + sys.exit(1) + + # At this point, we know all values are not None due to the validation above + assert uid_base_url is not None + assert uid_api_key is not None + assert uid_secret_key is not None + + return Config(uid_base_url, uid_api_key, uid_secret_key) diff --git a/static/examples/identity-map-integration-example/database.py b/static/examples/identity-map-integration-example/database.py new file mode 100644 index 000000000..0400ea627 --- /dev/null +++ b/static/examples/identity-map-integration-example/database.py @@ -0,0 +1,107 @@ +import sqlite3 +from dataclasses import dataclass + + +@dataclass +class IdentityToMap: + uid_mapping_id: int + dii: str + dii_type: str + + +def get_connection(db_path: str = "uid_mapping.db") -> sqlite3.Connection: + """Get SQLite database connection with Row factory enabled""" + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row # Enable dict-like access + return conn + + +def create_uid_mapping_table(conn: sqlite3.Connection) -> None: + """Create uid_mapping table with all necessary indexes""" + cursor = conn.cursor() + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS uid_mapping ( + uid_mapping_id INTEGER PRIMARY KEY AUTOINCREMENT, + dii TEXT NOT NULL, + dii_type TEXT NOT NULL CHECK (dii_type IN ('email', 'phone')), + current_uid TEXT, + previous_uid TEXT, + refresh_from TIMESTAMP, + opt_out BOOLEAN DEFAULT FALSE + ) + """ + ) + + cursor.execute( + """ + CREATE INDEX IF NOT EXISTS idx_mapping_query + ON uid_mapping(opt_out, current_uid, refresh_from) + """ + ) + + cursor.execute( + """ + CREATE INDEX IF NOT EXISTS idx_uid_mapping_current_uid + ON uid_mapping(current_uid) + """ + ) + + cursor.execute( + """ + CREATE INDEX IF NOT EXISTS idx_uid_mapping_previous_uid + ON uid_mapping(previous_uid) + """ + ) + + conn.commit() + + +def create_attribution_tables(conn: sqlite3.Connection) -> None: + """Create impressions and conversions tables with all necessary indexes""" + cursor = conn.cursor() + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS impressions ( + impression_id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL, + campaign_id TEXT NOT NULL + ) + """ + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS conversions ( + conversion_id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL, + conversion_value REAL NOT NULL + ) + """ + ) + + cursor.execute( + """ + CREATE INDEX IF NOT EXISTS idx_impressions_uid + ON impressions(uid) + """ + ) + + cursor.execute( + """ + CREATE INDEX IF NOT EXISTS idx_conversions_uid + ON conversions(uid) + """ + ) + + conn.commit() + + +def create_complete_database_schema(conn: sqlite3.Connection) -> None: + """Create complete database schema with all tables and indexes""" + create_uid_mapping_table(conn) + create_attribution_tables(conn) diff --git a/static/examples/identity-map-integration-example/env.example b/static/examples/identity-map-integration-example/env.example new file mode 100644 index 000000000..6b6d36e0e --- /dev/null +++ b/static/examples/identity-map-integration-example/env.example @@ -0,0 +1,3 @@ +UID2_BASE_URL=https://operator-integ.uidapi.com +UID2_API_KEY=your_api_key_here +UID2_SECRET_KEY=your_secret_key_here \ No newline at end of file diff --git a/static/examples/identity-map-integration-example/map_identities.py b/static/examples/identity-map-integration-example/map_identities.py new file mode 100644 index 000000000..7d44dc620 --- /dev/null +++ b/static/examples/identity-map-integration-example/map_identities.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +import sqlite3 +import sys +import traceback +from sqlite3 import Connection +from typing import Any, Tuple + +from uid2_client import ( + IdentityMapV3Input, + UnmappedIdentityReason, +) + +from config import load_config +from database import get_connection, IdentityToMap +from uid_client_wrapper import UIDClientWrapper + + +def map_identities(conn: Connection) -> None: + """ + Performs identity mapping in sequential batches: + - Fetches records from the database that need mapping. + - Splits records into batches (up to 5000 per batch). + - For each batch: + • Builds UID2 input and calls the UID2 mapping API. + • Updates mapped identities in the database. + • Marks opted-out identities. + - Prints summary statistics after processing all batches. + """ + config = load_config() + uid_client = UIDClientWrapper( + config.uid_base_url, config.uid_api_key, config.uid_secret_key + ) + + print("Fetching records that need mapping...") + all_records: list[IdentityToMap] = get_records_to_map(conn) + batches: list[list[IdentityToMap]] = batch_records(all_records) # batches of up to 5k records + print(f"Processing {len(all_records)} records in {len(batches)} batches...") + + total_processed = 0 + total_mapped = 0 + total_opted_out = 0 + + for batch_num, batch in enumerate(batches, 1): + try: + print(f"Processing batch {batch_num} of {len(batches)} ({len(batch)} records)...") + processed, mapped, opted_out = map_batch(batch, batch_num, conn, uid_client) + total_processed += processed + total_mapped += mapped + total_opted_out += opted_out + + except Exception as e: + print(f"Batch {batch_num} processing failed: {e}") + traceback.print_exc() + sys.exit(1) + + print(f"Mapping complete: processed={total_processed}, mapped={total_mapped}, opted_out={total_opted_out}") + +def map_batch( + batch: list[IdentityToMap], + batch_num: int, + conn: Connection, + uid_client: UIDClientWrapper, +) -> Tuple[int, int, int]: + """ + Processes a single batch of identity records. + + - Maps identities in the batch using the UID2 API. + - Updates the database with mapped identities. + - Marks identities as opted out if indicated by the API. + + Returns: + Tuple of (number processed, number mapped, number opted out) + """ + dii_to_id = {record.dii: record.uid_mapping_id for record in batch} + + uid_input, invalid_diis = build_uid_input(batch) + response = uid_client.identity_map(uid_input) + + mapped_identities = response.mapped_identities + + optout_ids = [] + invalid_count = len(invalid_diis) + + for dii, unmapped_identity in response.unmapped_identities.items(): + if unmapped_identity.reason == UnmappedIdentityReason.OPTOUT: + optout_ids.append(dii_to_id[dii]) + + if unmapped_identity.reason in [ + UnmappedIdentityReason.INVALID_IDENTIFIER, + UnmappedIdentityReason.UNKNOWN, + ]: + invalid_count += 1 + + update_mapped_records(conn, mapped_identities, dii_to_id) + update_optout_records(conn, optout_ids) + + processed_count = len(batch) + mapped_count = len(mapped_identities) + opted_out_count = len(optout_ids) + + print(f"Batch {batch_num} complete: {mapped_count} mapped, {opted_out_count} opted out, {invalid_count} invalid") + + return processed_count, mapped_count, opted_out_count + + +def batch_records(all_records: list[IdentityToMap]) -> list[list[IdentityToMap]]: + """Split records into batches of up to 5000""" + batch_size = 5000 + batches = [ + all_records[i : i + batch_size] for i in range(0, len(all_records), batch_size) + ] + return batches + + +def get_records_to_map(conn: sqlite3.Connection) -> list[IdentityToMap]: + """Retrieves records that either have never been mapped (current_uid IS NULL) + or need refresh (refresh_from in the past). Excludes opted out records.""" + cursor = conn.cursor() + + base_query = """ + SELECT uid_mapping_id, dii, dii_type + FROM uid_mapping + WHERE opt_out = FALSE + AND (current_uid IS NULL OR refresh_from < datetime('now')) \ + """ + + cursor.execute(base_query) + + return [ + IdentityToMap( + uid_mapping_id=row["uid_mapping_id"], + dii=row["dii"], + dii_type=row["dii_type"], + ) + for row in cursor.fetchall() + ] + + +def build_uid_input(records: list[IdentityToMap]) -> tuple[IdentityMapV3Input, list[str]]: + """Build UID input object from database records. + If adding an id causes a ValueError, add it to invalid_diis and continue. Other exceptions are raised.""" + + uid_input = IdentityMapV3Input() + invalid_diis = [] + + for record in records: + try: + if record.dii_type == "email": + uid_input.with_email(record.dii) + elif record.dii_type == "phone": + uid_input.with_phone(record.dii) + else: + raise Exception(f"Unknown dii type: {record.dii_type}") + except ValueError: + # ValueError is raised by uid2_client when the DII is invalid + invalid_diis.append(record.dii) + + return uid_input, invalid_diis + +def update_mapped_records( + conn: sqlite3.Connection, + mapped_identities: dict[str, Any], + dii_to_id: dict[str, int], +) -> None: + """Update database records with successful mapping results""" + if not mapped_identities: + return + + cursor = conn.cursor() + sql_data = [ + ( + mapped_identity.current_raw_uid, + mapped_identity.previous_raw_uid, + mapped_identity.refresh_from, + dii_to_id[dii], + ) + for dii, mapped_identity in mapped_identities.items() + ] + + cursor.executemany( + """ + UPDATE uid_mapping + SET current_uid = ?, + previous_uid = ?, + refresh_from = ? + WHERE uid_mapping_id = ? + """, + sql_data, + ) + + conn.commit() + print(f"Updated {len(mapped_identities)} mapped records") + + +def update_optout_records(conn: sqlite3.Connection, optout_ids: list[int]) -> None: + """Sets opt_out=TRUE and clears all UID-related fields to ensure + these records are never processed again.""" + if not optout_ids: + return + + cursor = conn.cursor() + sql_data = [(uid_mapping_id,) for uid_mapping_id in optout_ids] + + cursor.executemany( + """ + UPDATE uid_mapping + SET opt_out = TRUE, + current_uid = NULL, + previous_uid = NULL, + refresh_from = NULL + WHERE uid_mapping_id = ? + """, + sql_data, + ) + + conn.commit() + print(f"Marked {len(optout_ids)} records as opted out") + + +if __name__ == "__main__": + conn = get_connection("uid_mapping.db") + try: + map_identities(conn) + finally: + conn.close() diff --git a/static/examples/identity-map-integration-example/populate_test_conversions_impressions.py b/static/examples/identity-map-integration-example/populate_test_conversions_impressions.py new file mode 100644 index 000000000..4ccadadef --- /dev/null +++ b/static/examples/identity-map-integration-example/populate_test_conversions_impressions.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +UID Attribution Test Data Population + +Creates impression and conversion test data using actual mapped UIDs. +Should be run AFTER UID mapping is complete. +""" +import random +import sqlite3 +import traceback +from datetime import datetime, timedelta + +from database import get_connection, create_attribution_tables + + +def populate_attribution_data(conn: sqlite3.Connection) -> None: + """Generate simple attribution demo data using actual UIDs""" + cursor = conn.cursor() + + create_attribution_tables(conn) + + cursor.execute( + """ + SELECT DISTINCT current_uid FROM uid_mapping + WHERE current_uid IS NOT NULL AND opt_out = FALSE + LIMIT 10000 + """ + ) + uid_rows = cursor.fetchall() + + if not uid_rows: + print("No mapped UIDs found, skipping attribution data generation") + return + + sample_uids = [row[0] for row in uid_rows] + print(f"Using {len(sample_uids)} actual UIDs for attribution demo data") + + cursor.execute("DELETE FROM impressions") + cursor.execute("DELETE FROM conversions") + + def random_recent_timestamp(): + now = datetime.now() + days_ago = random.randint(0, 90) + random_time = now - timedelta(days=days_ago, seconds=random.randint(0, 86399)) + return random_time + + print("Generating impressions...") + impressions_data = [ + ( + random.choice(sample_uids), + random_recent_timestamp(), + f"camp_{random.randint(1, 5)}", + ) + for _ in range(1000) + ] + + print("Generating conversions...") + conversions_data = [ + ( + random.choice(sample_uids), + random_recent_timestamp(), + round(random.uniform(10.0, 500.0), 2), + ) + for _ in range(500) + ] + + cursor.executemany( + """ + INSERT INTO impressions (uid, timestamp, campaign_id) + VALUES (?, ?, ?) + """, + impressions_data, + ) + + cursor.executemany( + """ + INSERT INTO conversions (uid, timestamp, conversion_value) + VALUES (?, ?, ?) + """, + conversions_data, + ) + + conn.commit() + + print( + f"Generated {len(impressions_data):,} impressions and {len(conversions_data):,} conversions" + ) + + unique_impression_uids = len(set(uid for uid, _, _ in impressions_data)) + unique_conversion_uids = len(set(uid for uid, _, _ in conversions_data)) + + print( + f"Coverage: {unique_impression_uids} UIDs have impressions, {unique_conversion_uids} UIDs have conversions" + ) + + +def main(): + try: + conn = get_connection() + print("Populating attribution test data...") + populate_attribution_data(conn) + print("Attribution data population complete!") + + except Exception as e: + print(f"Attribution data population failed: {e}") + traceback.print_exc() + + finally: + if "conn" in locals(): + conn.close() + + +if __name__ == "__main__": + main() diff --git a/static/examples/identity-map-integration-example/populate_test_uid_mappings.py b/static/examples/identity-map-integration-example/populate_test_uid_mappings.py new file mode 100644 index 000000000..bb7f35fc7 --- /dev/null +++ b/static/examples/identity-map-integration-example/populate_test_uid_mappings.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +Test Data Population Script + +Creates 100k test records in uid_mapping database with following distribution: +- 10% already opted out +- 20% up for refresh (refresh_from in the past, current_uid populated) +- 20% never mapped (current_uid=NULL, refresh_from=NULL) +- 49% already mapped with future refresh dates +- 1% invalid DIIs (random strings) +""" +import random +import sqlite3 +import string +import traceback +import uuid +from datetime import datetime, timedelta +from typing import Optional + +from database import get_connection, create_uid_mapping_table + + +def generate_test_records() -> ( + list[tuple[str, str, Optional[str], Optional[str], Optional[datetime], bool]] +): + """Generate test uid to dii mapping records""" + records = [] + total_records = 100000 + opted_out_count = int(total_records * 0.10) + refresh_needed_count = int(total_records * 0.20) + never_mapped_count = int(total_records * 0.20) + invalid_dii_count = int(total_records * 0.01) + already_mapped_count = ( + total_records - opted_out_count - refresh_needed_count - never_mapped_count - invalid_dii_count + ) + + print(f"Generating test data distribution:") + print(f" Opted out: {opted_out_count}") + print(f" Up for refresh: {refresh_needed_count}") + print(f" Never mapped: {never_mapped_count}") + print(f" Invalid DIIs: {invalid_dii_count}") + print(f" Already mapped: {already_mapped_count}") + + # 1. Opted out records (10%) + for _ in range(opted_out_count): + dii, dii_type = generate_dii() + + records.append((dii, dii_type, None, None, None, True)) + + # 2. Up for refresh records (20%) + for _ in range(refresh_needed_count): + dii, dii_type = generate_dii() + current_uid = generate_uid() + previous_uid = generate_uid() if random.random() < 0.5 else None + refresh_from = datetime.now() - timedelta(days=random.randint(1, 30)) + + records.append((dii, dii_type, current_uid, previous_uid, refresh_from, False)) + + # 3. Never mapped records (20%) + for _ in range(never_mapped_count): + dii, dii_type = generate_dii() + + records.append((dii, dii_type, None, None, None, False)) + + # 4. Invalid DII records (1%) + for _ in range(invalid_dii_count): + dii, dii_type = generate_invalid_dii() + + records.append((dii, dii_type, None, None, None, False)) + + # 5. Already mapped with future refresh (49%) + for _ in range(already_mapped_count): + dii, dii_type = generate_dii() + current_uid = generate_uid() + previous_uid = generate_uid() if random.random() < 0.5 else None + refresh_from = datetime.now() + timedelta(days=random.randint(1, 365)) + + records.append((dii, dii_type, current_uid, previous_uid, refresh_from, False)) + + random.shuffle(records) + return records + + +def populate_database(conn: sqlite3.Connection) -> None: + """Populate database with test records""" + try: + cursor = conn.cursor() + + print("Clearing existing database...") + cursor.execute("DROP TABLE IF EXISTS uid_mapping") + + print("Recreating database schema...") + create_uid_mapping_table(conn) + + print("Generating 100k test records...") + records = generate_test_records() + + print("Inserting records into database...") + cursor.executemany( + """ + INSERT INTO uid_mapping (dii, dii_type, current_uid, previous_uid, refresh_from, opt_out) + VALUES (?, ?, ?, ?, ?, ?) + """, + records, + ) + + conn.commit() + + cursor.execute("SELECT COUNT(*) FROM uid_mapping") + total_count = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM uid_mapping WHERE opt_out = TRUE") + opted_out_count = cursor.fetchone()[0] + + cursor.execute( + "SELECT COUNT(*) FROM uid_mapping WHERE current_uid IS NULL AND refresh_from IS NULL AND opt_out = FALSE" + ) + never_mapped_count = cursor.fetchone()[0] + + cursor.execute( + "SELECT COUNT(*) FROM uid_mapping WHERE current_uid IS NOT NULL AND refresh_from < datetime('now')" + ) + refresh_needed_count = cursor.fetchone()[0] + + print(f"\nDatabase populated successfully!") + print(f"Total records: {total_count}") + print(f"Opted out: {opted_out_count}") + print(f"Never mapped: {never_mapped_count}") + print(f"Up for refresh: {refresh_needed_count}") + + except Exception as e: + print(f"Error populating database: {e}") + print("Full stack trace:") + traceback.print_exc() + raise + + +def generate_dii() -> tuple[str, str]: + """Generate a DII (email or phone) with its type""" + dii_type = random.choice(["email", "phone"]) + if dii_type == "email": + dii = generate_email() + else: + dii = generate_phone() + return dii, dii_type + + +def generate_invalid_dii() -> tuple[str, str]: + """Generate an invalid DII (random string)""" + dii_type = random.choice(["email", "phone"]) + dii = ''.join(random.choices(string.ascii_letters, k=10)) + return dii, dii_type + + +def generate_email() -> str: + """Generate a realistic email address""" + return f"{uuid.uuid4()}@test.com" + + +def generate_phone() -> str: + """Generate a realistic phone number""" + return f"+1{random.randint(2000000000, 9999999999)}" + + +def generate_uid() -> str: + """Generate a UID for testing""" + return f"uid_{random.randint(1000000, 9999999)}" + + +def main() -> None: + conn = get_connection("uid_mapping.db") + try: + populate_database(conn) + print("Test data population completed successfully!") + except Exception as e: + print(f"Failed to populate test data: {e}") + raise + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/static/examples/identity-map-integration-example/pyproject.toml b/static/examples/identity-map-integration-example/pyproject.toml new file mode 100644 index 000000000..45fcf0a3a --- /dev/null +++ b/static/examples/identity-map-integration-example/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "identity-map-tech-sample-2" +version = "0.1.0" +description = "UID2 Identity Map V3 technical sample demonstrating email/phone to UID2 mapping with proper optout handling" +requires-python = ">=3.13" +dependencies = [ + "python-dotenv>=1.0.0", + "uid2-client>=2.6.0", +] + +[dependency-groups] +dev = [ + "black>=23.0.0", +] diff --git a/static/examples/identity-map-integration-example/uid_client_wrapper.py b/static/examples/identity-map-integration-example/uid_client_wrapper.py new file mode 100644 index 000000000..e534fd9aa --- /dev/null +++ b/static/examples/identity-map-integration-example/uid_client_wrapper.py @@ -0,0 +1,52 @@ +import time +import urllib.error + +from uid2_client import IdentityMapV3Client, IdentityMapV3Input, IdentityMapV3Response + + +class UIDClientWrapper: + """Wrapper for UID2 client that handles retries""" + + def __init__(self, base_url: str, api_key: str, secret_key: str) -> None: + self.client = IdentityMapV3Client(base_url, api_key, secret_key) + + def identity_map(self, uid_input: IdentityMapV3Input) -> IdentityMapV3Response: + retry_delays = [1, 2, 4, 8, 16] # exponential backoff + max_retries = len(retry_delays) + + # HTTP status codes that should be retried (transient errors) + retryable_status_codes: set[int] = { + 429, # Too Many Requests (rate limiting) + 500, # Internal Server Error + 502, # Bad Gateway + 503, # Service Unavailable + 504, # Gateway Timeout + } + + for attempt in range(max_retries + 1): + try: + return self.client.generate_identity_map(uid_input) + + except urllib.error.HTTPError as http_error: + if attempt == max_retries: + print( + f"HTTP {http_error.code} error failed after {max_retries} retries: {http_error}" + ) + raise + + if http_error.code in retryable_status_codes: + print( + f"HTTP {http_error.code} error on attempt {attempt + 1}, retrying in {retry_delays[attempt]}s: {http_error}" + ) + time.sleep(retry_delays[attempt]) + continue + + print(f"Non-retryable HTTP {http_error.code} error: {http_error}") + raise + + except Exception as e: + print(f"Error: {e}") + raise + + # This line should never be reached, but included for type safety + raise RuntimeError("Unexpected exit from retry loop") From 2d72ae26a3718022aadbcba72fac2950341ae9e9 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Mon, 21 Jul 2025 14:04:15 +0800 Subject: [PATCH 2/9] Moved code to /src folder --- .../README.md | 51 +++++++++++++++---- .../{ => src}/attribution_analysis.py | 0 .../{ => src}/complete_demo.py | 0 .../{ => src}/config.py | 0 .../{ => src}/database.py | 0 .../{ => src}/map_identities.py | 0 .../populate_test_conversions_impressions.py | 0 .../{ => src}/populate_test_uid_mappings.py | 0 .../{ => src}/uid_client_wrapper.py | 0 9 files changed, 40 insertions(+), 11 deletions(-) rename static/examples/identity-map-integration-example/{ => src}/attribution_analysis.py (100%) rename static/examples/identity-map-integration-example/{ => src}/complete_demo.py (100%) rename static/examples/identity-map-integration-example/{ => src}/config.py (100%) rename static/examples/identity-map-integration-example/{ => src}/database.py (100%) rename static/examples/identity-map-integration-example/{ => src}/map_identities.py (100%) rename static/examples/identity-map-integration-example/{ => src}/populate_test_conversions_impressions.py (100%) rename static/examples/identity-map-integration-example/{ => src}/populate_test_uid_mappings.py (100%) rename static/examples/identity-map-integration-example/{ => src}/uid_client_wrapper.py (100%) diff --git a/static/examples/identity-map-integration-example/README.md b/static/examples/identity-map-integration-example/README.md index 24ab4646a..8e1a0623c 100644 --- a/static/examples/identity-map-integration-example/README.md +++ b/static/examples/identity-map-integration-example/README.md @@ -4,6 +4,23 @@ This sample shows a pattern for mapping email addresses and phone numbers to UID tokens, handling optouts, managing token refresh cycles, and performing a sample attribution analysis based on both current and previous UIDs. +## Project Structure + +``` +identity-map-integration-example/ +├── src/ # Python source code +│ ├── complete_demo.py # End-to-end demo workflow +│ ├── map_identities.py # Core UID2 mapping logic +│ ├── attribution_analysis.py # Attribution analysis example +│ ├── config.py # Configuration loading +│ ├── database.py # Database schema and utilities +│ ├── uid_client_wrapper.py # UID2 client with retry logic +│ └── populate_*.py # Test data generation scripts +├── .env # UID2 credentials (create from .env.example) +├── pyproject.toml # Project configuration +└── README.md # This file +``` + ## Quick Start ### 1. Install Dependencies @@ -31,7 +48,19 @@ UID2_SECRET_KEY=your_secret_key_here ### 3. Run Complete Demo ```bash # Full workflow: test data → UID2 mapping → attribution analysis -uv run complete_demo.py +uv run src/complete_demo.py +``` + +### 4. Run Individual Components +```bash +# Generate test data only +uv run src/populate_test_uid_mappings.py + +# Run UID mapping only +uv run src/map_identities.py + +# Run attribution analysis only +uv run src/attribution_analysis.py ``` ## Core UID2 Integration Patterns @@ -39,9 +68,9 @@ uv run complete_demo.py ### Identity Mapping Workflow **Key Integration Points:** -1. **Batch Processing** (`map_identities.py:build_uid2_input()`) - Process sequential batches of up to 5,000 DIIs per request -2. **Retry Logic** (`uid_client_wrapper.py:generate_identity_map_with_retry()`) - Exponential backoff for network resilience -3. **Response Handling** (`map_identities.py:process_uid2_response()`) - Process mapped, opted-out, and invalid identifiers +1. **Batch Processing** (`src/map_identities.py:build_uid2_input()`) - Process sequential batches of up to 5,000 DIIs per request +2. **Retry Logic** (`src/uid_client_wrapper.py:generate_identity_map_with_retry()`) - Exponential backoff for network resilience +3. **Response Handling** (`src/map_identities.py:process_uid2_response()`) - Process mapped, opted-out, and invalid identifiers ## Sample Database Schema @@ -52,9 +81,9 @@ CREATE TABLE uid_mapping ( dii TEXT NOT NULL, -- Email or phone (+E.164) dii_type TEXT NOT NULL, -- 'email' or 'phone' current_uid TEXT, -- Current UID2 token - previous_uid TEXT, -- Previous UID2 token (for attribution) + previous_uid TEXT, -- Previous UID2 token (only available for 90 days after rotation, afterwards NULL) refresh_from TIMESTAMP, -- When to refresh mapping - opt_out BOOLEAN DEFAULT FALSE -- Permanent optout flag + opt_out BOOLEAN DEFAULT FALSE -- The user has opted out, we shouldn't attempt to map them again ); ``` @@ -76,11 +105,11 @@ WHERE um.opt_out = FALSE; | Script | Purpose | Key Integration Concepts | |--------|---------|-------------------------| -| `populate_test_uid_mappings.py` | Creates 100k test records | Database schema, DII formatting | -| `map_identities.py` | **Core UID2 mapping logic** | Batch processing, retry logic, response handling | -| `populate_test_conversions_impressions.py` | Attribution demo data | UID2 token usage in measurement | -| `attribution_analysis.py` | Attribution analysis | Cross-UID joins, measurement patterns | -| `complete_demo.py` | End-to-end workflow | Full integration validation | +| `src/populate_test_uid_mappings.py` | Creates 100k test records | Database schema, DII formatting | +| `src/map_identities.py` | **Core UID2 mapping logic** | Batch processing, retry logic, response handling | +| `src/populate_test_conversions_impressions.py` | Attribution demo data | UID2 token usage in measurement | +| `src/attribution_analysis.py` | Attribution analysis | Cross-UID joins, measurement patterns | +| `src/complete_demo.py` | End-to-end workflow | Full integration validation | ## Production Integration Checklist diff --git a/static/examples/identity-map-integration-example/attribution_analysis.py b/static/examples/identity-map-integration-example/src/attribution_analysis.py similarity index 100% rename from static/examples/identity-map-integration-example/attribution_analysis.py rename to static/examples/identity-map-integration-example/src/attribution_analysis.py diff --git a/static/examples/identity-map-integration-example/complete_demo.py b/static/examples/identity-map-integration-example/src/complete_demo.py similarity index 100% rename from static/examples/identity-map-integration-example/complete_demo.py rename to static/examples/identity-map-integration-example/src/complete_demo.py diff --git a/static/examples/identity-map-integration-example/config.py b/static/examples/identity-map-integration-example/src/config.py similarity index 100% rename from static/examples/identity-map-integration-example/config.py rename to static/examples/identity-map-integration-example/src/config.py diff --git a/static/examples/identity-map-integration-example/database.py b/static/examples/identity-map-integration-example/src/database.py similarity index 100% rename from static/examples/identity-map-integration-example/database.py rename to static/examples/identity-map-integration-example/src/database.py diff --git a/static/examples/identity-map-integration-example/map_identities.py b/static/examples/identity-map-integration-example/src/map_identities.py similarity index 100% rename from static/examples/identity-map-integration-example/map_identities.py rename to static/examples/identity-map-integration-example/src/map_identities.py diff --git a/static/examples/identity-map-integration-example/populate_test_conversions_impressions.py b/static/examples/identity-map-integration-example/src/populate_test_conversions_impressions.py similarity index 100% rename from static/examples/identity-map-integration-example/populate_test_conversions_impressions.py rename to static/examples/identity-map-integration-example/src/populate_test_conversions_impressions.py diff --git a/static/examples/identity-map-integration-example/populate_test_uid_mappings.py b/static/examples/identity-map-integration-example/src/populate_test_uid_mappings.py similarity index 100% rename from static/examples/identity-map-integration-example/populate_test_uid_mappings.py rename to static/examples/identity-map-integration-example/src/populate_test_uid_mappings.py diff --git a/static/examples/identity-map-integration-example/uid_client_wrapper.py b/static/examples/identity-map-integration-example/src/uid_client_wrapper.py similarity index 100% rename from static/examples/identity-map-integration-example/uid_client_wrapper.py rename to static/examples/identity-map-integration-example/src/uid_client_wrapper.py From b39b9276d2d3f5d0737b4db0acc8bff84e6b725c Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Tue, 22 Jul 2025 15:50:55 +0800 Subject: [PATCH 3/9] Added references to sample from various docs --- .../integration-advertiser-dataprovider-endpoints.md | 7 +++++++ .../guides/integration-advertiser-dataprovider-overview.md | 7 +++++++ docs/sdks/sdk-ref-java.md | 7 +++++++ docs/sdks/sdk-ref-python.md | 6 ++++++ static/examples/identity-map-integration-example/README.md | 2 +- 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/guides/integration-advertiser-dataprovider-endpoints.md b/docs/guides/integration-advertiser-dataprovider-endpoints.md index 295ef05b3..b249192d5 100644 --- a/docs/guides/integration-advertiser-dataprovider-endpoints.md +++ b/docs/guides/integration-advertiser-dataprovider-endpoints.md @@ -58,6 +58,13 @@ DII refers to a user's normalized email address or phone number, or the normaliz +## Integration Example + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +While the sample is implemented using the Python SDK, the integration patterns are applicable to any SDK or direct API integration. + +For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ### 1: Generate Raw UID2s from DII | Step | Endpoint | Description | diff --git a/docs/guides/integration-advertiser-dataprovider-overview.md b/docs/guides/integration-advertiser-dataprovider-overview.md index ad77d0850..fb9c8e220 100644 --- a/docs/guides/integration-advertiser-dataprovider-overview.md +++ b/docs/guides/integration-advertiser-dataprovider-overview.md @@ -60,6 +60,13 @@ The following table shows the implementation options that are available for adve | [5: Monitor for Raw UID2 Refresh](#5-monitor-for-raw-uid2-refresh) | Use the refresh timestamp (`r` field) returned from the [POST /identity/map](../endpoints/post-identity-map.md) endpoint to determine when to refresh Raw UID2s. | | [6: Monitor for Opt-Out Status](#6-monitor-for-opt-out-status) | API call to the [POST /optout/status](../endpoints/post-optout-status.md) endpoint. | +## Integration Example + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +While the sample is implemented using the Python SDK, the integration patterns and applicable to any SDK or direct API integration. + +For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ## Integration Diagram The following diagram outlines the steps that data collectors must complete to map DII to raw UID2s for audience building and targeting. diff --git a/docs/sdks/sdk-ref-java.md b/docs/sdks/sdk-ref-java.md index 48f2dfd2b..0336b7174 100644 --- a/docs/sdks/sdk-ref-java.md +++ b/docs/sdks/sdk-ref-java.md @@ -350,6 +350,13 @@ If you're using server-side integration (see [Publisher Integration Guide, Serve } ``` +## Integration Example + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +While the sample is implemented using the Python SDK, the integration patterns are applicable to any SDK or direct API integration. + +For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + >**Note:** The SDK automatically handles email normalization and hashing, ensuring that raw email addresses and phone numbers do not leave your server. ### Usage Example diff --git a/docs/sdks/sdk-ref-python.md b/docs/sdks/sdk-ref-python.md index 6741639db..45823ebbb 100644 --- a/docs/sdks/sdk-ref-python.md +++ b/docs/sdks/sdk-ref-python.md @@ -301,6 +301,12 @@ mixed_input = IdentityMapV3Input() mixed_response = client.generate_identity_map(mixed_input) ``` +### Integration Example + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). + +For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ## Migration From Version Using v2 Identity Map The following sections provide general information and guidance for migrating to the latest version of this SDK, which references `POST /identity/map` version 3, including: diff --git a/static/examples/identity-map-integration-example/README.md b/static/examples/identity-map-integration-example/README.md index 8e1a0623c..2370f7c9f 100644 --- a/static/examples/identity-map-integration-example/README.md +++ b/static/examples/identity-map-integration-example/README.md @@ -47,7 +47,7 @@ UID2_SECRET_KEY=your_secret_key_here ### 3. Run Complete Demo ```bash -# Full workflow: test data → UID2 mapping → attribution analysis +# Full workflow: test data population → UID2 mapping → attribution analysis uv run src/complete_demo.py ``` From 8ae1c19c16490ee51657b582dafbd24785c4bcda Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Tue, 22 Jul 2025 15:55:18 +0800 Subject: [PATCH 4/9] Better wording --- .../examples/identity-map-integration-example/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/examples/identity-map-integration-example/README.md b/static/examples/identity-map-integration-example/README.md index 2370f7c9f..050e82a8f 100644 --- a/static/examples/identity-map-integration-example/README.md +++ b/static/examples/identity-map-integration-example/README.md @@ -113,11 +113,11 @@ WHERE um.opt_out = FALSE; ## Production Integration Checklist -**Critical Requirements for UID2 Integration:** +**Patterns for UID2 Integration:** ✅ **Request Limits**: Maximum 5,000 DIIs per request ✅ **Sequential Processing**: No parallel requests to UID2 service ✅ **Retry Logic**: Exponential backoff for network failures -✅ **Optout Handling**: Permanent exclusion from future processing -✅ **Token Refresh**: Respect `refresh_from` timestamps -✅ **State Persistence**: Track mapping state between runs +✅ **Optout Handling**: Permanent exclude opted out users from future processing +✅ **Token Refresh**: Re-map tokens when they reach `refresh_from` timestamps +✅ **State Persistence**: Track mapping state From 85882135970b63f23890ad21c62928ecf70728da Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 24 Jul 2025 13:50:48 +0800 Subject: [PATCH 5/9] Addressing Gen's feedback --- ...egration-advertiser-dataprovider-endpoints.md | 8 ++++---- ...tegration-advertiser-dataprovider-overview.md | 8 ++++---- docs/sdks/sdk-ref-java.md | 14 +++++++------- docs/sdks/sdk-ref-python.md | 4 ++-- .../identity-map-integration-example/README.md | 16 ++++++++-------- .../pyproject.toml | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/guides/integration-advertiser-dataprovider-endpoints.md b/docs/guides/integration-advertiser-dataprovider-endpoints.md index b249192d5..1ddd07df0 100644 --- a/docs/guides/integration-advertiser-dataprovider-endpoints.md +++ b/docs/guides/integration-advertiser-dataprovider-endpoints.md @@ -58,12 +58,12 @@ DII refers to a user's normalized email address or phone number, or the normaliz -## Integration Example +### Integration Example -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -While the sample is implemented using the Python SDK, the integration patterns are applicable to any SDK or direct API integration. +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. -For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). +For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). ### 1: Generate Raw UID2s from DII diff --git a/docs/guides/integration-advertiser-dataprovider-overview.md b/docs/guides/integration-advertiser-dataprovider-overview.md index fb9c8e220..c3e9826a1 100644 --- a/docs/guides/integration-advertiser-dataprovider-overview.md +++ b/docs/guides/integration-advertiser-dataprovider-overview.md @@ -60,12 +60,12 @@ The following table shows the implementation options that are available for adve | [5: Monitor for Raw UID2 Refresh](#5-monitor-for-raw-uid2-refresh) | Use the refresh timestamp (`r` field) returned from the [POST /identity/map](../endpoints/post-identity-map.md) endpoint to determine when to refresh Raw UID2s. | | [6: Monitor for Opt-Out Status](#6-monitor-for-opt-out-status) | API call to the [POST /optout/status](../endpoints/post-optout-status.md) endpoint. | -## Integration Example +### Integration Example -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -While the sample is implemented using the Python SDK, the integration patterns and applicable to any SDK or direct API integration. +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. -For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). +For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). ## Integration Diagram diff --git a/docs/sdks/sdk-ref-java.md b/docs/sdks/sdk-ref-java.md index 0336b7174..4baa26fb1 100644 --- a/docs/sdks/sdk-ref-java.md +++ b/docs/sdks/sdk-ref-java.md @@ -350,13 +350,6 @@ If you're using server-side integration (see [Publisher Integration Guide, Serve } ``` -## Integration Example - -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -While the sample is implemented using the Python SDK, the integration patterns are applicable to any SDK or direct API integration. - -For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). - >**Note:** The SDK automatically handles email normalization and hashing, ensuring that raw email addresses and phone numbers do not leave your server. ### Usage Example @@ -392,6 +385,13 @@ IdentityMapV3Input mixedInput = new IdentityMapV3Input() IdentityMapV3Response mixedResponse = client.generateIdentityMap(mixedInput); ``` +### Integration Example + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. + +For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ## Migration From Version Using v2 Identity Map The following sections provide general information and guidance for migrating to the latest version of this SDK, which references `POST /identity/map` version 3, including: diff --git a/docs/sdks/sdk-ref-python.md b/docs/sdks/sdk-ref-python.md index 45823ebbb..6696fb57d 100644 --- a/docs/sdks/sdk-ref-python.md +++ b/docs/sdks/sdk-ref-python.md @@ -303,9 +303,9 @@ mixed_response = client.generate_identity_map(mixed_input) ### Integration Example -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map V3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -For step-by-step setup instructions and to run the example, see the [Integration Example README](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). +For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). ## Migration From Version Using v2 Identity Map diff --git a/static/examples/identity-map-integration-example/README.md b/static/examples/identity-map-integration-example/README.md index 050e82a8f..81e33c065 100644 --- a/static/examples/identity-map-integration-example/README.md +++ b/static/examples/identity-map-integration-example/README.md @@ -1,8 +1,8 @@ # UID2 Integration Technical Sample -**Complete UID2 integration example demonstrating Identity Map V3 flow.** +**Complete UID2 integration example demonstrating Identity Map v3 flow.** -This sample shows a pattern for mapping email addresses and phone numbers to UID tokens, handling optouts, managing token refresh cycles, and performing a sample attribution analysis based on both current and previous UIDs. +This sample shows a pattern for mapping email addresses and phone numbers to UID2 tokens, handling optouts, managing token refresh cycles, and performing a sample attribution analysis based on both current and previous UID2s. ## Project Structure @@ -56,7 +56,7 @@ uv run src/complete_demo.py # Generate test data only uv run src/populate_test_uid_mappings.py -# Run UID mapping only +# Run UID2 mapping only uv run src/map_identities.py # Run attribution analysis only @@ -68,7 +68,7 @@ uv run src/attribution_analysis.py ### Identity Mapping Workflow **Key Integration Points:** -1. **Batch Processing** (`src/map_identities.py:build_uid2_input()`) - Process sequential batches of up to 5,000 DIIs per request +1. **Batch Processing** (`src/map_identities.py:build_uid2_input()`) - Process sequential batches of up to 5,000 emails and/or phone numbers per request 2. **Retry Logic** (`src/uid_client_wrapper.py:generate_identity_map_with_retry()`) - Exponential backoff for network resilience 3. **Response Handling** (`src/map_identities.py:process_uid2_response()`) - Process mapped, opted-out, and invalid identifiers @@ -83,7 +83,7 @@ CREATE TABLE uid_mapping ( current_uid TEXT, -- Current UID2 token previous_uid TEXT, -- Previous UID2 token (only available for 90 days after rotation, afterwards NULL) refresh_from TIMESTAMP, -- When to refresh mapping - opt_out BOOLEAN DEFAULT FALSE -- The user has opted out, we shouldn't attempt to map them again + opt_out BOOLEAN DEFAULT FALSE -- The user has opted out, we shouldn't attempt to map this user again ); ``` @@ -95,7 +95,7 @@ FROM uid_mapping WHERE opt_out = FALSE AND (current_uid IS NULL OR refresh_from < datetime('now')); --- Attribution joins using both current and previous UIDs +-- Attribution joins using both current and previous UID2s SELECT * FROM impressions imp JOIN uid_mapping um ON (imp.uid2 = um.current_uid OR imp.uid2 = um.previous_uid) WHERE um.opt_out = FALSE; @@ -115,9 +115,9 @@ WHERE um.opt_out = FALSE; **Patterns for UID2 Integration:** -✅ **Request Limits**: Maximum 5,000 DIIs per request +✅ **Request Limits**: Maximum 5,000 emails and/or phone numbers per request ✅ **Sequential Processing**: No parallel requests to UID2 service ✅ **Retry Logic**: Exponential backoff for network failures ✅ **Optout Handling**: Permanent exclude opted out users from future processing -✅ **Token Refresh**: Re-map tokens when they reach `refresh_from` timestamps +✅ **Raw UID2 Refresh**: Re-map raw UID2s when they reach `refresh_from` timestamps ✅ **State Persistence**: Track mapping state diff --git a/static/examples/identity-map-integration-example/pyproject.toml b/static/examples/identity-map-integration-example/pyproject.toml index 45fcf0a3a..bbd07d5ce 100644 --- a/static/examples/identity-map-integration-example/pyproject.toml +++ b/static/examples/identity-map-integration-example/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "identity-map-tech-sample-2" version = "0.1.0" -description = "UID2 Identity Map V3 technical sample demonstrating email/phone to UID2 mapping with proper optout handling" +description = "UID2 Identity Map v3 technical sample demonstrating email/phone to UID2 mapping with proper optout handling" requires-python = ">=3.13" dependencies = [ "python-dotenv>=1.0.0", From 0ead1ec329c82c8206ac4aa99313ec7d1ef0ae3d Mon Sep 17 00:00:00 2001 From: genwhittTTD Date: Fri, 25 Jul 2025 12:56:46 -0400 Subject: [PATCH 6/9] implemented snippet + resequenced in Adv guides --- .../integration-advertiser-dataprovider-endpoints.md | 12 +++++------- .../integration-advertiser-dataprovider-overview.md | 12 +++++------- docs/sdks/sdk-ref-java.md | 6 ++---- docs/sdks/sdk-ref-python.md | 5 ++--- docs/snippets/_integration-example-identity-map.mdx | 9 +++++++++ 5 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 docs/snippets/_integration-example-identity-map.mdx diff --git a/docs/guides/integration-advertiser-dataprovider-endpoints.md b/docs/guides/integration-advertiser-dataprovider-endpoints.md index 1ddd07df0..c1ca3a61d 100644 --- a/docs/guides/integration-advertiser-dataprovider-endpoints.md +++ b/docs/guides/integration-advertiser-dataprovider-endpoints.md @@ -8,6 +8,7 @@ displayed_sidebar: sidebarAdvertisers --- import Link from '@docusaurus/Link'; +import IntegrationExampleIdentityMap from '../snippets/_integration-example-identity-map.mdx'; # Advertiser/Data Provider Integration to HTTP Endpoints @@ -32,6 +33,10 @@ You'll need to set up these values, in the UID2 Portal on the [API Keys](../port It's very important that you keep these values secure. For details, see [Security of API Key and Client Secret](../getting-started/gs-credentials.md#security-of-api-key-and-client-secret). ::: +## Integration Example + + + ## High-Level Steps At a high level, the steps for advertisers and data providers integrating with UID2 are as follows: @@ -58,13 +63,6 @@ DII refers to a user's normalized email address or phone number, or the normaliz -### Integration Example - -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. - -For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). - ### 1: Generate Raw UID2s from DII | Step | Endpoint | Description | diff --git a/docs/guides/integration-advertiser-dataprovider-overview.md b/docs/guides/integration-advertiser-dataprovider-overview.md index c3e9826a1..290ff0912 100644 --- a/docs/guides/integration-advertiser-dataprovider-overview.md +++ b/docs/guides/integration-advertiser-dataprovider-overview.md @@ -8,6 +8,7 @@ displayed_sidebar: sidebarAdvertisers --- import Link from '@docusaurus/Link'; +import IntegrationExampleIdentityMap from '../snippets/_integration-example-identity-map.mdx'; # Advertiser/Data Provider Integration Overview @@ -27,6 +28,10 @@ There are other ways that you can use UID2, outside these use cases. These are j | Send in conversions | Send raw UID2s as conversion information | Use conversion information for measurement (attribution) or for retargeting via API or pixels. | | Receive graph data | Receive raw UID2s from graph/data providers via API or pixels | Build graph data. | +## Integration Example + + + ## High-Level Steps At a high level, the steps for advertisers and data providers integrating with UID2 are as follows: @@ -60,13 +65,6 @@ The following table shows the implementation options that are available for adve | [5: Monitor for Raw UID2 Refresh](#5-monitor-for-raw-uid2-refresh) | Use the refresh timestamp (`r` field) returned from the [POST /identity/map](../endpoints/post-identity-map.md) endpoint to determine when to refresh Raw UID2s. | | [6: Monitor for Opt-Out Status](#6-monitor-for-opt-out-status) | API call to the [POST /optout/status](../endpoints/post-optout-status.md) endpoint. | -### Integration Example - -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. - -For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). - ## Integration Diagram The following diagram outlines the steps that data collectors must complete to map DII to raw UID2s for audience building and targeting. diff --git a/docs/sdks/sdk-ref-java.md b/docs/sdks/sdk-ref-java.md index 4baa26fb1..c32f6c804 100644 --- a/docs/sdks/sdk-ref-java.md +++ b/docs/sdks/sdk-ref-java.md @@ -7,6 +7,7 @@ displayed_sidebar: docs --- import Link from '@docusaurus/Link'; +import IntegrationExampleIdentityMap from '../snippets/_integration-example-identity-map.mdx'; # SDK for Java Reference Guide @@ -387,10 +388,7 @@ IdentityMapV3Response mixedResponse = client.generateIdentityMap(mixedInput); ### Integration Example -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). -The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. - -For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ## Migration From Version Using v2 Identity Map diff --git a/docs/sdks/sdk-ref-python.md b/docs/sdks/sdk-ref-python.md index 6696fb57d..218ef6a63 100644 --- a/docs/sdks/sdk-ref-python.md +++ b/docs/sdks/sdk-ref-python.md @@ -7,6 +7,7 @@ displayed_sidebar: docs --- import Link from '@docusaurus/Link'; +import IntegrationExampleIdentityMap from '../snippets/_integration-example-identity-map.mdx'; # SDK for Python Reference Guide @@ -303,9 +304,7 @@ mixed_response = client.generate_identity_map(mixed_input) ### Integration Example -For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). - -For step-by-step setup instructions and to run the example, see [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). + ## Migration From Version Using v2 Identity Map diff --git a/docs/snippets/_integration-example-identity-map.mdx b/docs/snippets/_integration-example-identity-map.mdx new file mode 100644 index 000000000..c7d01236d --- /dev/null +++ b/docs/snippets/_integration-example-identity-map.mdx @@ -0,0 +1,9 @@ + + +import Link from '@docusaurus/Link'; + +For a complete demonstration of a working integration that includes all the recommended patterns, see the [UID2 Identity Map v3 Integration Example](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example). + +The sample uses the Python SDK, but the integration patterns are applicable to any SDK or direct API integration. + +For step-by-step setup instructions and to run the example, see the README.md file: [UID2 Integration Technical Sample](https://github.com/IABTechLab/uid2docs/blob/main/static/examples/identity-map-integration-example/README.md). From b67e514b16abc056738a4662e4ff4989f144475f Mon Sep 17 00:00:00 2001 From: genwhittTTD Date: Mon, 28 Jul 2025 10:06:33 -0400 Subject: [PATCH 7/9] fix build issue due to missing import statement --- docs/sdks/sdk-ref-java.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sdks/sdk-ref-java.md b/docs/sdks/sdk-ref-java.md index 543a7a9c9..b702a16f4 100644 --- a/docs/sdks/sdk-ref-java.md +++ b/docs/sdks/sdk-ref-java.md @@ -7,6 +7,7 @@ displayed_sidebar: docs --- import Link from '@docusaurus/Link'; +import IntegrationExampleIdentityMap from '../snippets/_integration-example-identity-map.mdx'; import POSTIdentityMapImprovements from '../snippets/_post-identity-map-improvements-v3.mdx'; # SDK for Java Reference Guide From af5fa1e64013a8016590bb629c7293628dd979fc Mon Sep 17 00:00:00 2001 From: sophia chen Date: Tue, 29 Jul 2025 11:55:04 +1000 Subject: [PATCH 8/9] updated links --- docs/ref-info/deprecation-schedule.md | 6 +++--- .../{env.example => .env.example} | 0 static/examples/identity-map-integration-example/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename static/examples/identity-map-integration-example/{env.example => .env.example} (100%) diff --git a/docs/ref-info/deprecation-schedule.md b/docs/ref-info/deprecation-schedule.md index a16cf7c2b..4b70b92ec 100644 --- a/docs/ref-info/deprecation-schedule.md +++ b/docs/ref-info/deprecation-schedule.md @@ -41,7 +41,7 @@ The latest ZIP file is available in the Assets section at the bottom of the link | Q3 2024 | [v5.38.104](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.38.104) | 5.38.104 | September 12, 2024 | Mar 31, 2026 | | Q2 2024 | [v5.37.12](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.37.12) | 5.37.12 | June 12, 2024 | Sep 30, 2025 | -For documentation, see [UID2 Private Operator for AWS Integration Guide](..\guides\operator-guide-aws-marketplace.md). +For documentation, see [UID2 Private Operator for AWS Integration Guide](../guides/operator-guide-aws-marketplace.md). ### Private Operator for GCP @@ -55,7 +55,7 @@ The latest ZIP file is linked in the GCP Download column in the following table. | Q3 2024 | [v5.38.104](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.38.104) | [gcp-oidc-deployment-files-5.38.104.zip](https://github.com/IABTechLab/uid2-operator/releases/download/v5.38.104/gcp-oidc-deployment-files-5.38.104.zip) | September 12, 2024 | Mar 31, 2026 | | Q2 2024 | [v5.37.12](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.37.12) | [gcp-oidc-deployment-files-5.37.12.zip](https://github.com/IABTechLab/uid2-operator/releases/download/v5.37.12/gcp-oidc-deployment-files-5.37.12.zip) | June 12, 2024 | Sep 30, 2025 | -For documentation, see [UID2 Private Operator for GCP Integration Guide](..\guides\operator-private-gcp-confidential-space.md). +For documentation, see [UID2 Private Operator for GCP Integration Guide](../guides/operator-private-gcp-confidential-space.md). ### Private Operator for Azure @@ -69,7 +69,7 @@ The latest ZIP file is linked in the Azure Download column in the following tabl | Q3 2024 | [v5.38.104](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.38.104) | [azure-cc-deployment-files-5.38.104.zip](https://github.com/IABTechLab/uid2-operator/releases/download/v5.38.104/azure-cc-deployment-files-5.38.104.zip) | September 12, 2024 | Mar 31, 2026 | | Q2 2024 | [v5.37.12](https://github.com/IABTechLab/uid2-operator/releases/tag/v5.37.12) | [azure-cc-deployment-files-5.37.12.zip](https://github.com/IABTechLab/uid2-operator/releases/download/v5.37.12/azure-cc-deployment-files-5.37.12.zip) | June 12, 2024 | Sep 30, 2025 | -For documentation, see [UID2 Private Operator for Azure Integration Guide](..\guides\operator-guide-azure-enclave.md). +For documentation, see [UID2 Private Operator for Azure Integration Guide](../guides/operator-guide-azure-enclave.md).