点击追踪:广告点击的"数字足迹"

每一次点击,都是用户与广告的一次"握手"。如何记录这次握手,决定了我们能否真正理解用户的选择。

引言

在数字广告的世界里,点击是最基础的交互行为。用户看到广告,产生兴趣,点击——这一连串动作看似简单,背后却是一整套精密的追踪系统在运转。

点击追踪(Click Tracking)是广告归因的基石。没有准确的点击记录,后续的归因分析、效果评估、投放优化都将成为无本之木。今天,我们就来聊聊这个看似简单、实则暗藏玄机的技术。


一、点击追踪的作用

1.1 为什么需要追踪点击?

想象一下这样的场景:

某游戏公司同时在 A、B、C 三个广告平台投放广告。一天下来,游戏新增了 1000 名用户。那么问题来了:这 1000 个用户分别是从哪个平台来的?哪个平台的效果更好?后续付费的用户又是从哪个渠道转化的?

没有点击追踪,这些问题都无法回答。

点击追踪的核心价值在于:

1.2 点击追踪的层级

点击追踪不是单一维度的记录,而是多层级的信息采集:

三个层级各有用途:宏观指导预算分配,中观优化创意方向,微观支持个案分析和反作弊。一个完善的追踪系统需要同时支持这三个层级的数据采集和查询。

1.3 点击追踪的业务价值

从业务角度,点击追踪直接支撑以下场景:


二、点击追踪的技术原理

2.1 追踪链接的工作机制

点击追踪的本质,是在用户点击广告时"插入"一个记录环节。这就像在高速公路上设置一个收费站,每辆车经过都会被记录,然后继续上路。

最常见的方式是追踪链接(Tracking URL)。当用户点击广告时,实际上访问的是一个追踪服务器地址,追踪服务器记录点击信息后,再通过 HTTP 重定向将用户送往真正的落地页。

整个过程可以概括为:

  1. 用户看到广告,产生点击行为
  2. 浏览器/应用向追踪服务器发起请求
  3. 追踪服务器解析请求,提取点击信息
  4. 将点击信息写入存储系统
  5. 返回 302 重定向,指向落地页
  6. 用户到达落地页,完成跳转

这个流程对用户来说是透明的,通常只需几百毫秒。但从技术角度看,每一步都需要精心设计。

一个典型的追踪链接长这样:

https://track.example.com/click?
    campaign_id=12345
    &creative_id=67890
    &channel=tiktok
    &callback_url=https%3A%2F%2Fgame.com%2Fdownload
    &click_id={clickid}

这个URL包含几个关键部分:

当用户点击这个链接时,追踪服务器会:

  1. 解析URL参数,提取广告信息
  2. 生成唯一的Click ID
  3. 记录完整的点击数据
  4. 返回302重定向到落地页

2.2 点击标识的生成

每次点击需要一个唯一标识(Click ID),这是后续归因的"身份证"。

生成 Click ID 需要考虑几个问题:

常见的做法是使用 UUID 或 Snowflake 算法生成 ID,再配合 Base64 或类似编码压缩长度。有些系统还会在 ID 中嵌入时间戳和渠道标识,便于快速定位和分片。

// 使用Snowflake算法生成Click ID
type ClickIDGenerator struct {
    workerID    int64 // 机器ID
    datacenterID int64 // 数据中心ID
    sequence    int64 // 序列号
    lastTimestamp int64 // 上次生成时间戳
}

func (g *ClickIDGenerator) NextID() string {
    // 64位ID结构:
    // 0 - 41位时间戳 - 10位机器ID - 12位序列号
    timestamp := time.Now().UnixMilli() - epoch
    
    if timestamp == g.lastTimestamp {
        g.sequence = (g.sequence + 1) & 4095
        if g.sequence == 0 {
            timestamp = g.waitNextMillis()
        }
    } else {
        g.sequence = 0
    }
    
    g.lastTimestamp = timestamp
    
    id := (timestamp << 22) | 
          (g.datacenterID << 17) |
          (g.workerID << 12) |
          g.sequence
    
    return base64.RawURLEncoding.EncodeToString(
        binary.BigEndian.AppendUint64(nil, id)
    )
}

这个实现的特点:

有些系统会在Click ID中编码更多信息:

// Click ID结构:时间戳(36位) + 渠道ID(12位) + 随机数(16位)
function encodeClickID(timestamp, channelID, random) {
    const timePart = timestamp.toString(36);  // 36进制编码
    const channelPart = channelID.toString(36).padStart(3, '0');
    const randomPart = random.toString(36).padStart(4, '0');
    
    return `${timePart}-${channelPart}-${randomPart}`;
    // 示例:lq3x9k2-abc-f7h2
}

function decodeClickID(clickID) {
    const [timeStr, channelStr, randomStr] = clickID.split('-');
    
    return {
        timestamp: parseInt(timeStr, 36),
        channelID: parseInt(channelStr, 36),
        random: parseInt(randomStr, 36)
    };
}

这种方式的优势:

2.3 信息传递方式

点击采集到的信息需要传递给后续环节,主要有两种方式:

实际系统中,往往是两种方式的结合:核心信息服务端存储,关键标识客户端透传作为备份和校验。

追踪链接 → 追踪服务器 → 落地页URL
                ↓
            数据库存储
                ↓
        Click ID: abc123

落地页URL示例:
https://game.com/download?click_id=abc123&channel=tiktok&campaign=12345

客户端SDK在落地页初始化时:

class AttributionSDK {
    init() {
        // 1. 从URL提取参数
        const urlParams = new URLSearchParams(window.location.search);
        const clickID = urlParams.get('click_id');
        const channel = urlParams.get('channel');
        
        // 2. 验证服务端数据
        this.verifyClick(clickID).then(serverData => {
            // 3. 对比客户端和服务端数据
            if (serverData.channel !== channel) {
                this.reportMismatch(clickID, channel, serverData.channel);
            }
            
            // 4. 使用服务端数据作为基准
            this.attributionData = serverData;
        });
    }
    
    async verifyClick(clickID) {
        const response = await fetch(
            `https://track.example.com/verify/${clickID}`
        );
        return response.json();
    }
}

这种方案的优势:


三、追踪链路设计

3.1 端到端的追踪链路

一个完整的点击追踪链路包含多个环节,每个环节都有其职责和挑战:

广告展示 → 用户点击 → 追踪服务器 → 数据处理 → 归因匹配
    ↓          ↓            ↓            ↓            ↓
  展示ID    Click ID     存储记录      清洗聚合     转化关联

3.2 多跳追踪场景

在实际业务中,追踪链路往往更加复杂:

广告主 → DSP平台 → 广告联盟 → 流量主 → 用户
   ↓         ↓          ↓         ↓
Track A   Track B    Track C   Track D

每一级都需要记录点击,形成完整的证据链:

// 多跳追踪的URL示例
const multiHopURL = 
    `https://track.advertiser.com/click?` +
    `dsp_click_id=xyz&` +          // DSP的点击ID
    `network_click_id=abc&` +      // 广告联盟的点击ID
    `publisher_click_id=def&` +    // 流量主的点击ID
    `final_url=${encodeURIComponent(landingPage)}`;

// 追踪服务器处理
function handleMultiHopClick(req) {
    // 1. 记录自己的Click ID
    const myClickID = generateClickID();
    
    // 2. 保存上游的点击ID
    const upstreamIDs = {
        dsp: req.query.dsp_click_id,
        network: req.query.network_click_id,
        publisher: req.query.publisher_click_id
    };
    
    // 3. 存储完整链路
    db.save({
        click_id: myClickID,
        upstream: upstreamIDs,
        timestamp: Date.now()
    });
    
    // 4. 重定向到下一跳或落地页
    redirect(req.query.final_url);
}

移动端应用常使用深度链接直接打开App:

点击广告 → 追踪服务器 → Universal Link → App启动 → 归因SDK上报

Universal Link的处理:

// iOS Universal Link处理
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }
    
    // 1. 提取追踪参数
    let clickID = url.queryParameters["click_id"]
    let channel = url.queryParameters["channel"]
    
    // 2. 调用归因SDK
    AttributionSDK.shared.handleDeepLink(
        clickID: clickID,
        channel: channel
    )
    
    return true
}

3.3 数据流转保证

追踪链路中,数据可能在任何环节丢失。需要设计容错机制:

追踪服务器 → 消息队列 → 数据处理 → 归因系统
     ↓            ↓           ↓
   确认1        确认2       确认3

只有收到所有确认,才算追踪完成。任何环节失败都会重试。

客户端SDK维护本地队列,失败请求自动重试:

class ReliableTrackingSDK {
    constructor() {
        this.pendingQueue = this.loadQueue();
        this.retryInterval = setInterval(() => this.retry(), 5000);
    }
    
    trackClick(clickData) {
        // 1. 先存本地
        this.pendingQueue.push({
            id: clickData.click_id,
            data: clickData,
            retries: 0,
            timestamp: Date.now()
        });
        this.saveQueue();
        
        // 2. 尝试上报
        this.sendToServer(clickData);
    }
    
    async sendToServer(data) {
        try {
            await fetch('/track/click', {
                method: 'POST',
                body: JSON.stringify(data)
            });
            
            // 成功后从队列移除
            this.removeFromQueue(data.click_id);
        } catch (error) {
            console.log('Track failed, will retry later');
        }
    }
    
    retry() {
        this.pendingQueue
            .filter(item => item.retries < 5)
            .forEach(item => {
                this.sendToServer(item.data);
                item.retries++;
            });
        this.saveQueue();
    }
}

四、点击数据采集

4.1 需要采集哪些信息?

一次点击携带的信息远比想象中丰富。大致可以分为几类:

4.2 不同平台的采集挑战

// iOS 14.5+ 需要请求用户授权
import AppTrackingTransparency
import AdSupport

class IDFAManager {
    func requestIDFA() {
        ATTrackingManager.requestTrackingAuthorization { status in
            switch status {
            case .authorized:
                let idfa = ASIdentifierManager.shared().advertisingIdentifier
                self.sendIDFA(idfa.uuidString)
            case .denied, .restricted, .notDetermined:
                // IDFA不可用,使用备选方案
                self.useFallbackIdentifier()
            @unknown default:
                break
            }
        }
    }
    
    func useFallbackIdentifier() {
        // 方案1: 使用概率性归因
        // 方案2: 使用SKAdNetwork
        // 方案3: 使用自己的用户ID体系
    }
}
// 国内Android设备OAID获取
class OAIDHelper(context: Context) {
    fun getOAID(callback: (String?) -> Unit) {
        try {
            // 1. 尝试MSA SDK(移动安全联盟)
            val supplier = MdidSdkHelper.getMdidSupplier()
            if (supplier != null) {
                supplier.getOAID(context, object : IIdentifierListener {
                    override fun onSupport(oaid: String?, isSupported: Boolean) {
                        callback(if (isSupported) oaid else null)
                    }
                })
            } else {
                // 2. 尝试各厂商自有方案
                getManufacturerOAID(callback)
            }
        } catch (e: Exception) {
            callback(null)
        }
    }
    
    private fun getManufacturerOAID(callback: (String?) -> Unit) {
        when (Build.MANUFACTURER.lowercase()) {
            "huawei", "honor" -> getHuaweiOAID(callback)
            "xiaomi" -> getXiaomiOAID(callback)
            "oppo" -> getOPPOOAID(callback)
            "vivo" -> getVIVOOAID(callback)
            else -> callback(null)
        }
    }
}

4.3 采集时机的选择

什么时候采集点击信息?看似简单,实际有讲究:

实际系统往往采用混合策略:核心信息即时采集保证归因时效,辅助信息异步补全降低延迟,极端情况下批量采集作为兜底。

type ClickCollector struct {
    coreFields    []string // 核心字段,即时采集
    enrichFields  []string // 补充字段,异步采集
    batchFields   []string // 批量字段,延迟采集
}

func (c *ClickCollector) Collect(req *http.Request) *ClickData {
    click := &ClickData{
        ClickID:   generateClickID(),
        Timestamp: time.Now(),
    }
    
    // 第一级:核心字段(即时)
    for _, field := range c.coreFields {
        click.SetField(field, extractFromRequest(req, field))
    }
    
    // 第二级:补充字段(异步队列)
    go func() {
        time.Sleep(10 * time.Millisecond) // 不影响响应
        for _, field := range c.enrichFields {
            value := c.enrichField(field, req)
            click.SetField(field, value)
        }
        c.saveEnrichedData(click)
    }()
    
    // 第三级:批量字段(定时任务)
    c.queueForBatchProcessing(click)
    
    return click
}

// 字段分级示例
var defaultCollector = &ClickCollector{
    coreFields: []string{
        "click_id", "timestamp", "campaign_id", 
        "channel", "ip", "user_agent",
    },
    enrichFields: []string{
        "device_brand", "os_version", "isp",
        "geo_city", "geo_country",
    },
    batchFields: []string{
        "fingerprint", "risk_score", 
        "detailed_ua",
    },
}

五、点击数据的存储与处理

5.1 存储架构设计

点击数据有三个典型特征:

针对这些特征,存储架构通常采用分层设计:

写入 → Kafka → Flink → 多个存储后端
                     ├→ Redis (热数据,7天)
                     ├→ ClickHouse (温数据,30天)
                     └→ S3 (冷数据,归档)
存储类型 适用场景 优势 劣势
Redis 热数据、实时归因 极快查询 内存成本高
ClickHouse 分析查询 压缩率高、聚合快 不适合频繁更新
Elasticsearch 全文检索 灵活查询 资源消耗大
MySQL/TiDB 事务场景 可靠性高 写入性能有限
S3/OSS 归档存储 成本低 查询需要恢复

5.2 数据分片策略

海量点击数据需要分片存储,常见的分片维度:

实际系统往往是时间分片为主,辅以其他维度。分片策略还需要考虑后续的数据迁移和扩容便利性。

-- 按日期分表(最常见)
CREATE TABLE clicks_20260301 (
    click_id VARCHAR(64) PRIMARY KEY,
    campaign_id INT,
    channel VARCHAR(32),
    timestamp DATETIME,
    ip VARCHAR(64),
    device_info JSON,
    INDEX idx_campaign (campaign_id),
    INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB;

-- 自动分表脚本
CREATE EVENT create_daily_table
ON SCHEDULE EVERY 1 DAY
STARTS '2026-03-02 00:00:00'
DO
  SET @sql = CONCAT(
    'CREATE TABLE clicks_', 
    DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 DAY), '%Y%m%d'),
    ' LIKE clicks_template'
  );
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
  DEALLOCATE PREPARE stmt;
type ClickSharding struct {
    nodes       []string
    ring        *hashring.HashRing
    replicas    int
}

func (s *ClickSharding) GetShard(clickID string) string {
    // 使用Click ID进行一致性哈希
    key, _ := s.ring.GetNode(clickID)
    return key
}

func (s *ClickSharding) AddNode(node string) {
    s.ring.AddNode(node, s.replicas)
    // 一致性哈希只影响部分数据迁移
}

5.3 数据处理流程

点击数据从采集到可用,需要经过一系列处理:

整个流程通常是流批一体的架构:实时流处理保证归因的时效性,批处理保证分析的准确性和完整性。

# Flink实时处理作业
pipeline:
  name: click-processing
  
  source:
    type: kafka
    topic: clicks-raw
    bootstrap: kafka:9092
    
  operators:
    - name: parse
      type: map
      function: ParseClickData
      
    - name: validate
      type: filter
      function: IsValidClick
      
    - name: deduplicate
      type: keyed_process
      key: click_id
      function: DeduplicateClick
      state_ttl: 24h
      
    - name: enrich_geo
      type: async_map
      function: EnrichGeoData
      timeout: 100ms
      
    - name: enrich_device
      type: async_map
      function: EnrichDeviceInfo
      timeout: 100ms
      
    - name: detect_anomaly
      type: map
      function: DetectAnomaly
      
  sinks:
    - name: redis
      type: redis
      key_pattern: "click:${click_id}"
      ttl: 7d
      
    - name: clickhouse
      type: clickhouse
      table: clicks
      batch_size: 10000
      
    - name: kafka_output
      type: kafka
      topic: clicks-processed
class ClickValidator:
    def __init__(self):
        self.bot_patterns = [
            r'bot', r'crawler', r'spider', r'scraper'
        ]
        self.suspicious_ips = set()  # 黑名单IP
        
    def validate(self, click):
        """验证点击是否有效"""
        
        # 1. 基础字段检查
        if not all([click.click_id, click.timestamp, click.campaign_id]):
            return False, "missing_required_fields"
        
        # 2. 时间合理性
        if click.timestamp > time.time() + 300:  # 未来5分钟
            return False, "future_timestamp"
        if click.timestamp < time.time() - 86400:  # 24小时前
            return False, "too_old"
        
        # 3. IP黑名单
        if click.ip in self.suspicious_ips:
            return False, "blacklisted_ip"
        
        # 4. User-Agent检查
        ua_lower = click.user_agent.lower()
        for pattern in self.bot_patterns:
            if pattern in ua_lower:
                return False, "bot_detected"
        
        # 5. 参数完整性
        if not self.valid_url(click.landing_page):
            return False, "invalid_landing_page"
        
        return True, "valid"
    
    def deduplicate(self, click, window_seconds=60):
        """去重检查"""
        key = f"dedup:{click.ip}:{click.campaign_id}"
        
        # 使用Redis检查窗口内是否已存在
        if redis.exists(key):
            return False, "duplicate"
        
        redis.setex(key, window_seconds, "1")
        return True, "unique"

5.4 存储优化技巧

点击数据有很多重复内容,压缩可以大幅节省存储:

// 字典编码:将重复字符串转为数字ID
type DictionaryEncoder struct {
    dict map[string]uint32
    nextID uint32
}

func (e *DictionaryEncoder) Encode(value string) uint32 {
    if id, exists := e.dict[value]; exists {
        return id
    }
    id := e.nextID
    e.dict[value] = id
    e.nextID++
    return id
}

// 列式存储:相同类型数据存储在一起
type ColumnarClickStorage struct {
    clickIDs   []string
    timestamps []int64
    campaignIDs []uint32  // 字典编码后
    channels   []uint32   // 字典编码后
    ipAddrs    []net.IP
}
-- ClickHouse分区表示例
CREATE TABLE clicks (
    click_id String,
    timestamp DateTime,
    campaign_id UInt32,
    channel LowCardinality(String),
    ip String,
    device_info String
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)  -- 按天分区
ORDER BY (timestamp, click_id)      -- 排序键
SETTINGS index_granularity = 8192;

六、常见问题与解决方案

6.1 点击丢失问题

# Prometheus监控指标
metrics:
  - name: click_tracking_success_rate
    type: gauge
    description: 点击追踪成功率
    alert:
      threshold: 99%
      severity: critical
      
  - name: click_tracking_latency_p99
    type: histogram
    description: 追踪延迟P99
    alert:
      threshold: 200ms
      severity: warning
      
  - name: click_redirect_success_rate
    type: gauge
    description: 重定向成功率
    alert:
      threshold: 99.5%
      severity: critical

6.2 点击去重问题

type Deduplicator struct {
    redis    *redis.Client
    localCache *lru.Cache  // 本地缓存减轻Redis压力
}

func (d *Deduplicator) IsDuplicate(clickID string) bool {
    // 第一层:本地缓存(快速)
    if _, exists := d.localCache.Get(clickID); exists {
        return true
    }
    
    // 第二层:Redis(准确)
    key := fmt.Sprintf("dedup:%s", clickID)
    exists := d.redis.SetNX(key, "1", 24*time.Hour).Val()
    
    if !exists {
        // Redis中已存在,是重复
        return true
    }
    
    // 写入本地缓存
    d.localCache.Add(clickID, struct{}{})
    return false
}

// 布隆过滤器方案(海量数据)
type BloomFilterDedup struct {
    filter *bloom.BloomFilter
}

func (d *BloomFilterDedup) MightExist(clickID string) bool {
    // 可能存在(有假阳性)
    return d.filter.Test([]byte(clickID))
}

func (d *BloomFilterDedup) Add(clickID string) {
    d.filter.Add([]byte(clickID))
}

6.3 时间同步问题

type TimeSynchronizer struct {
    ntpServers []string
    offset     time.Duration
}

func (t *TimeSynchronizer) Sync() error {
    var offsets []time.Duration
    
    for _, server := range t.ntpServers {
        response, err := ntp.Query(server)
        if err != nil {
            continue
        }
        offsets = append(offsets, response.ClockOffset)
    }
    
    if len(offsets) == 0 {
        return errors.New("all NTP servers failed")
    }
    
    // 取中位数
    sort.Slice(offsets, func(i, j int) bool {
        return offsets[i] < offsets[j]
    })
    t.offset = offsets[len(offsets)/2]
    
    return nil
}

func (t *TimeSynchronizer) Now() time.Time {
    return time.Now().Add(t.offset)
}

// 使用示例
var timeSync *TimeSynchronizer

func init() {
    timeSync = &TimeSynchronizer{
        ntpServers: []string{
            "time.apple.com",
            "time.google.com",
            "pool.ntp.org",
        },
    }
    go func() {
        for {
            timeSync.Sync()
            time.Sleep(time.Hour)
        }
    }()
}

6.4 隐私合规问题

type PrivacyCompliance struct {
    ipMask       bool
    encryptKeys  map[string]string
    retentionDays int
}

func (p *PrivacyCompliance) ProcessClick(click *ClickData) *ClickData {
    result := click.Clone()
    
    // 1. IP脱敏(只保留前3段)
    if p.ipMask {
        parts := strings.Split(click.IP, ".")
        if len(parts) == 4 {
            result.IP = strings.Join(parts[:3], ".") + ".0"
        }
    }
    
    // 2. 敏感字段加密
    for field, key := range p.encryptKeys {
        if value := click.GetField(field); value != "" {
            encrypted := aesEncrypt(value, key)
            result.SetField(field, encrypted)
        }
    }
    
    // 3. 添加过期时间
    result.ExpireAt = time.Now().AddDate(0, 0, p.retentionDays)
    
    return result
}

// 数据自动清理
func (p *PrivacyCompliance) StartCleanup(db *Database) {
    ticker := time.NewTicker(24 * time.Hour)
    go func() {
        for range ticker.C {
            db.DeleteWhere("expire_at < ?", time.Now())
        }
    }()
}
compliance_checklist:
  gdpr:
    - 用户明确同意
    - 提供数据删除选项
    - 数据可携带性
    - 隐私政策透明
    
  ccpa:
    - 告知数据收集
    - 提供退出选项
    - 不得歧视行使用户权利的用户
    
  china_pipl:
    - 最小必要原则
    - 明确告知用途
    - 获得用户授权
    - 敏感信息单独同意

七、总结与最佳实践

7.1 核心要点回顾

点击追踪是广告归因系统的"眼睛",每一次点击记录都是在为后续分析积累数据资产。没有准确的点击数据,再精妙的归因算法也只是空中楼阁。

一套好的点击追踪系统,应该具备:

技术实现上,需要在性能与准确性、实时性与成本、功能丰富与隐私合规之间找到平衡点。没有完美的方案,只有最适合当前业务阶段的选择。

7.2 技术选型建议

根据业务规模,推荐不同的技术栈:

7.3 最佳实践清单

7.4 常见陷阱警示

7.5 未来趋势

点击追踪技术也在不断演进:


八、实战案例分享

8.1 案例:某游戏公司的追踪系统升级

旧架构:同步写入MySQL → 分析慢、扩展难
新架构:Kafka → Flink → Redis(热) + ClickHouse(冷)
// 三重保障
type ClickTracker struct {
    kafka    *KafkaProducer  // 主通道
    localDisk *FileBuffer    // 本地缓存
    retryQueue *RetryQueue   // 重试队列
}

func (t *ClickTracker) Track(click *ClickData) error {
    // 1. 本地先存(防丢失)
    t.localDisk.Append(click)
    
    // 2. 发送Kafka
    if err := t.kafka.Send(click); err != nil {
        // 3. 失败则入重试队列
        t.retryQueue.Add(click)
        return err
    }
    
    // 4. 成功则删除本地缓存
    t.localDisk.Remove(click.ID)
    return nil
}

8.2 案例:跨境电商的多渠道追踪

// 记录用户完整点击路径
class MultiTouchTracker {
    constructor() {
        this.touchpoints = [];
    }
    
    trackClick(click) {
        this.touchpoints.push({
            clickID: click.id,
            channel: click.channel,
            campaign: click.campaign,
            timestamp: Date.now(),
            // ...其他信息
        });
        
        // 存储到本地
        localStorage.setItem('touchpoints', JSON.stringify(this.touchpoints));
    }
    
    // 转化时上报完整路径
    reportConversion(conversion) {
        const path = this.touchpoints.map(tp => ({
            click_id: tp.clickID,
            channel: tp.channel,
            time_to_conversion: conversion.timestamp - tp.timestamp
        }));
        
        fetch('/api/conversion', {
            method: 'POST',
            body: JSON.stringify({
                conversion: conversion,
                touchpoint_path: path
            })
        });
    }
}

结语

最后一点感悟:点击追踪看似是"记录"的技术,实则是"理解"的艺术。每一次点击背后,都是一个真实用户的选择和意图。我们要做的,是用技术手段忠实地记录这些选择,为后续的分析和决策提供可靠依据。

点击追踪不是目的,而是手段。真正的价值在于:

技术的最终目标,是让每一次点击都有价值,让每一分广告费都花在刀刃上。

下一篇,我们将深入探讨归因匹配的核心算法,看看如何将点击与转化关联起来,完成广告归因的"最后一公里"。



💬 评论 (0)

0/500
排序: