mcpField
A
ae-migrate-tracking-code

ae-migrate-tracking-code

github.com/thinkingaiagenticengine开发21280 次安装6 天前更新
代码仓库

关于 ae-migrate-tracking-code

ae-migrate-tracking-code 是面向已有第三方埋点代码的迁移技能,可将受支持的 SDK 调用整理为中间表示并映射到 AE。它覆盖从扫描调用点到协调计划生成与代码插入的流程。

主要功能

  1. 01识别受支持的第三方分析 SDK
  2. 02提取事件、身份与属性调用点
  3. 03追踪封装函数的调用位置
  4. 04将扫描结果整理为中间表示
  5. 05协调埋点计划与 AE 代码插入

适用场景

  1. 01将 Firebase Analytics 埋点切换到 AE
  2. 02把 Amplitude 事件迁移到 AE
  3. 03在保留原 SDK 时新增 AE 埋点
  4. 04扫描现有代码并整理迁移映射

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkingaiagenticengine/ae-cli --skill ae-migrate-tracking-code

安装后在 Claude Code 中输入/ae-migrate-tracking-code确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
ae-preset-properties.md
ir.md
mapping-framework.md
references/providers/
amplitude.md
firebase.md
ga4.md
mixpanel.md
README.md
sensors-data.md

详细介绍

ae-migrate-tracking-code

对话语言:本文档使用英文,但所有面向用户的输出都必须使用用户输入的语言。 用户输入英文 → 用英文回复;用户输入中文 → 用中文回复;用户输入日文 → 用日文回复。 如果不确定,则默认使用英文。 这适用于所有输出:章节标题、阶段名称、提示词、代码注释等。 ⚠️ 重要:许多提供商适配器文档包含中文术语说明。阅读这些文档并回答使用英文或日文的用户时,请将标题和注释翻译成用户使用的语言。 不要在英文或日文回复中照搬本文档中的中文内容。

术语表

中文EnglishNotes
埋点迁移Tracking Migration将现有第三方 SDK 埋点调用转换为 AE SDK 调用
源 SDKSource SDK / Provider项目中已有的第三方 SDK(Firebase、Amplitude 等)
调用点Call Site源 SDK 调用(logEvent / setUserId / setUserProperty / Identify)
归一化中间表示IR (Intermediate Representation)与提供商无关的已提取调用点转储
映射Mapping源事件/属性 → AE 事件/属性的转换
切换Switch将源调用替换为 AE track(),并移除源 SDK
双写 / 新增Add / Dual-write保留源 SDK,并同时添加 AE track()
依赖标识Dependency Identifier切换模式下用于移除源 SDK 的包/模块名称
自动采集事件Auto-track Event源 SDK 自动采集的事件(例如 session_start)
公共属性Super Property自动附加到每个事件的属性。⚠️ AE 中正确的中文术语是“公共属性”或“公共事件属性”。绝不能将“Super Property”翻译为“超级属性”——这不是有效的 AE 术语。
用户属性User Property设置在用户资料上的属性
用户体系User Identity SystemAE 中的 distinct_id / account_id 策略

触发条件

当用户提出以下请求时触发:“将 Firebase 埋点迁移到 AE / 用 ThinkingAI 替换 Amplitude / 我的项目已经有 Firebase/Amplitude 埋点,将其切换到 AE SDK / 在现有分析工具旁新增 AE 埋点”等,例如:把当前工程里的 xx SDK 切换到数数、把已有埋点用数数实现一遍、用数数替换 xx SDK、集成数数并参考已有的 xx SDK 埋点。

目标 AE SDK 也可能称为:数数 / 数数 SDK / ThinkingAI / ThinkingAI SDK / Agentic Engine / Agentic Engine SDK / AE / AE SDK——它们指的是同一个 SDK。触发信号是“已有的第三方源 SDK + 迁移/切换/替换/实现一遍”,或者“集成/接入数数,并参考已有的 xx SDK 埋点”。单纯接入数数 SDK / 加数数埋点,但没有源 SDK,属于从零开始的场景——应使用 ae-generate-tracking-code,而不是此技能。

范围:此技能仅处理代码库——扫描现有第三方埋点调用(阶段 0/1)。如果没有包含第三方埋点的源代码(只有产品文档/描述),请勿使用此技能;应改用 ae-generate-tracking-plan + ae-generate-tracking-code。将历史数据导入 AE 属于离线数据迁移工作,不在此技能的范围内——此技能只切换实时客户端/服务端 SDK,同时保留用户 ID 以及事件/属性名称,使新数据能够与导入的数据合并。

此技能负责协调端到端的迁移,但不会重复实现以下两个现有技能:

  • 方案生成/上传 → 移交给 ae-generate-tracking-plan(代码库路径)。
  • AE SDK 代码插入 → 移交给 ae-generate-tracking-code(插入模式),并使用迁移映射提供插入位置,而不是通过 grep 查找发现业务触发点。

提供方注册表(可扩展)

核心部分与提供方无关。每个第三方平台对应一个适配器文件。要支持新的提供方,请在 references/providers/ 下添加一个适配器,并在下方添加一行——不要修改此 SKILL.md。

提供方适配器覆盖的平台检测模式(摘要)
Firebase Analyticsreferences/providers/firebase.mdWeb v8/v9、Android、iOS、Flutter、React Native、Unity、C++firebase/analytics、firebase.analytics()、FirebaseAnalytics、FIRAnalytics、Analytics.logEvent(iOS Swift)、FirebaseAnalytics.instance
Amplitudereferences/providers/amplitude.mdWeb legacy(amplitude-js)、Browser 2.x、Node、Python、Go、Java、Android、iOS、React Native、Flutter、Unityamplitude-js、@amplitude/analytics-browser、@amplitude/analytics-node、amplitude.init、amplitude.getInstance()、amplitude.logEvent、amplitude.track、Identify、服务端导入(from amplitude import、analytics-go、com.amplitude:java-sdk)
Sensors Data(神策)references/providers/sensors-data.mdWeb、Android、iOS、Harmony、macOS/tvOS、C++、React Native、Flutter、Unity、Unreal、Cocos2d-x、小程序(微信/其他)、QuickApp、APICloud、uni-app、Weex、Egret、LayaAir,以及服务端 SDK(Java/Python/Go/Node/PHP/Ruby/C/.NET/Lua)sensorsdata.min.js、sensors.init、sensors.track、SensorsDataAPI、SensorsAnalyticsSDK、profileSet、setProfile(legacy)、服务端 sa-sdk-*(Python/Go/Node/PHP/Ruby/C/.NET/Lua)
Mixpanelreferences/providers/mixpanel.mdJavaScript、Node、Python、Go、Java、Ruby、PHP、Android、iOS、Swift、React Native、Flutter、Unitymixpanel-browser、mixpanel.init、mixpanel.track、mixpanel.identify、mixpanel.people、MixpanelAPI、mixpanel.NewApiClient(Go)、Mixpanel::Tracker(Ruby)、Mixpanel::getInstance(PHP)、mp.track / people_set(Python)
Google Analytics 4references/providers/ga4.mdWeb(gtag.js / dataLayer)、Measurement Protocol(服务端)gtag('event'、gtag('config'、gtag('set'、dataLayer(仅当同时存在 gtag(/G- 时才属于 GA4)、G-XXXXXXX、Measurement Protocol mp/collect

适配器契约(每个适配器必需的字段)定义于 references/providers/README.md。

支持范围:仅支持上面列出的提供方/平台(以及各适配器 Recognition 部分中的平台表格)。未列出的内容不在范围内,本技能不予支持——不要为未列出的平台编造映射。


阶段 0 — 检测来源 SDK 和版本

  1. 定位项目及其包/依赖清单(package.json、build.gradle、Podfile、pubspec.yaml、*.csproj 等),以及源代码导入项。
  2. 与上方注册表匹配。阅读匹配适配器的 Recognition 部分,确认提供方并确定版本变体:
    • Firebase:区分 v8 命名空间形式与 v9 模块化形式(Web),以及 Android/iOS/Flutter 原生形式。
    • Amplitude:区分 legacy(getInstance().logEvent)与 SDK 2.x(amplitude.track / @amplitude/analytics-browser)。
  3. 如果存在多个提供方,列出它们并询问用户先迁移哪一个(每次运行迁移一个提供方,映射更清晰)。例外——GA4 和 Firebase Analytics 属于同一数据流:GA4 是 Firebase 的 Web 层(同一个测量 ID 既是 GA4 媒体资源,也是 Firebase 项目)。如果同一项目中同时存在 gtag(...) 和 logEvent(...),它们不是两个提供方——应合并为一次迁移运行:一次扫描、一个 draft.json / mapping.json,并对共享的事件 / 属性 / 用户 ID 名称执行一次去重。切勿为一个 Google 数据流生成两份草稿。
  4. 如果没有匹配的注册表项,但存在跟踪代码 → 展示找到的调用,询问用户提供方名称,并根据 references/providers/README.md,在继续之前请用户提供文档/代码片段,以收集最小化适配器。

阶段 1 — 将调用点提取到 IR

根据适配器的 Recognition 模式,扫描项目中的四类调用:

  1. 事件调用:logEvent(...) / track(...) 及其参数/属性。
  2. 身份调用:setUserId(...) / identify(...) / login(...)。
  3. 用户属性调用:setUserProperty(...) / setUserProperties(...) / Identify 操作。
  4. 超级属性调用(常见事件属性):register(...) / registerSuperProperties(...)(Mixpanel、Sensors Data)/ setDefaultEventParameters(...)(Firebase)。将其记录为 kind: super_property,并保留完整的 props 映射——不要误读为用户属性(参见 mapping-framework.md §6)。Amplitude 和 GA4 没有此类 API。

将每个调用点记录为 file:line、调用类型、事件/参数值(字面量或通过阅读周边代码解析的变量名)。按照 references/ir.md 将规范化后的转储写入 .ae-cli/migration/scan.json。

包装层解析(反向调用图追踪)——实际项目几乎不会在每个业务事件中直接调用源 SDK,而是会将其包装在分析辅助函数中(trackEvent(name, props) → mixpanel.track(name, props))。简单的 grep 只能看到包装函数中唯一的 SDK 调用(事件名称 = 参数 → 会得到 resolved:false),从而遗漏所有真实业务事件。因此,当 SDK 调用的事件名称是一个函数参数(运行时变量从外层函数的参数传入)时,不要立即记录为 resolved:false:

  • 找出外层函数 F——仅当 F 的唯一作用是将该参数转发给 SDK 调用时,才将其视为分析包装函数。如果事件名称来自局部逻辑/表达式,而非参数,则它不是包装函数——按常规将该调用点记录为 resolved:false。
  • grep 查找 F 的所有调用者。将每个使用静态事件名称的调用者解析为普通调用点:kind 根据 F 所转发到的 SDK 调用推断(event / identity / user_property),event_name 取自调用者的字面量或常量参数,source_call = 调用者的调用行,file = 调用者的文件,via_wrapper = F 的名称。
  • 使用动态事件名称的调用者仍记为 resolved:false。如果没有找到调用者(包装函数被外部调用,或使用动态分派),则退回到将包装函数自身的 SDK 调用记录为一个 resolved:false 调用点。
  • 将 F 本身记录在 scan.json 顶层的 wrappers 数组中(参见 references/ir.md)——它是追踪的关键节点,不是迁移目标。

自动跟踪说明:源 SDK 自动采集的事件(例如 Firebase 的 session_start、first_open、screen_view)不是调用点。请在阶段 2 中根据适配器的 Auto-track decision 表进行处理,不要在此处处理。

阶段 2 — 映射到 AE(编写 draft.json + mapping.json)

  1. 阅读 references/mapping-framework.md(通用算法)和提供方适配器(差异)。
  2. 对每个提取出的事件:映射到 AE 事件(使用 snake_case 名称;需要前向兼容时则使用原样名称——mapping-framework.md §1;display_name 使用用户的语言);将每个参数映射为带类型的 AE 属性;并应用适配器中的命名约束(保留前缀、特殊字符、长度上限)。
  3. 应用去重和提升规则:同一属性出现在 3 个或更多事件中 → 公共事件属性(超级属性);预设的 # 属性永远不会成为超级属性。显式超级属性调用点(register / setDefaultEventParameters)也应放入 draft.json 的 common_event_properties 池中——参见 mapping-framework.md §6。
  4. 将身份映射到 AE login(accountId)(account_id_source user_account);如果需要与导入的历史数据保持前向兼容,则还要保持访客 ID 连续性(初始化时调用 setDistinctId / identify)——参见 mapping-framework.md §4。用户属性操作根据适配器的映射表,映射到 AE user_set / user_setOnce / user_add / user_append / user_unset。超级属性调用映射到 setSuperProperties({...})(静态公共事件属性),参见 mapping-framework.md §6。
  5. 根据调用点所在位置确定每个事件的 platform(应用代码 → 客户端;后端 → 服务端)。
  6. 请用户确认迁移模式(参见下方“模式”),并确认历史来源平台数据是否已(或将)离线导入 AE——这将决定根据 mapping-framework.md §1 采用前向兼容命名,以及根据 §4 保持访客 ID 连续性。导入本身属于离线数据迁移工作,不在范围内;此技能只切换实时 SDK——然后:
    • 编写 .ae-cli/migration/mapping.json(来源位置 → AE 事件 + 模式决策,结构见 references/ir.md)。
    • 按 AE 计划架构编写 .ae-cli/draft.json(与 ae-generate-tracking-plan 生成的架构相同),以便计划技能可直接使用。

展示映射摘要表,并在继续之前取得用户的 ok。

阶段 3 — 生成并上传追踪计划

将工作移交给 ae-generate-tracking-plan,使用已生成的代码库信息和 draft:

  1. ae-cli tracking plan draft --in .ae-cli/draft.json --out .ae-cli/draft.xlsx
  2. ae-cli tracking plan validate --in .ae-cli/draft.json --fix
  3. 根据计划技能确定 AE 主机、登录信息和 projectId,然后运行 ae-cli tracking plan upload ...(上传前询问用户;根据该技能的冲突检测决定追加还是替换)。

阶段 4 — 插入 AE SDK 代码(切换或添加)

将工作移交给 ae-generate-tracking-code(插入模式),但插入位置取自 .ae-cli/migration/mapping.json,而不是通过 grep 发现业务触发点:

  • 客户端/服务端平台和语言已在 draft.json meta 中;按该技能的方式确定 APP_ID / SERVER_URL。
  • 对于每个映射后的 AE 事件,插入点均为 mapping.json 中记录的原始调用点。 switch 模式(替换)——高风险,删除前必须确认:
  • 删除任何内容前,明确列出将删除的内容(源代码调用点,以及适配器 Dependency identifiers 中的源 SDK 依赖),并征得用户同意后再删除。
  • 在同一位置将源 logEvent/track 调用替换为 AE track(...) 调用;移除现已不再使用的源导入。
  • 将来自模块常量的事件名内联:在 AE track(...) 调用中直接写出 snake_case 字面名称(必须与 // @tracking <event_name> 标记一致)。删除常量定义前,先在整个项目中搜索其名称——模块常量通常会被多个文件导入、传入服务端报告,或在测试中断言。只有当迁移后的调用点是它仅有的引用时,才移除该常量(此时它属于死代码);否则保留。不要保留仅供迁移后的调用使用的常量。
  • 使用适配器的 Dependency identifiers 移除源 SDK 依赖(例如 firebase/analytics、@amplitude/analytics-browser)。
  • 移除无用的包装函数:当通过某个包装函数解析出的所有调用点都已执行 replace 时,该包装函数便成为死代码——将其删除(并移除现在不再使用的源导入)。如果该包装函数所在文件中没有其他内容,则删除该文件。(add 模式会保留包装函数——它仍为源 SDK 提供数据——并在每个业务调用点添加 AE 调用。)
  • 类似地,将身份识别/用户属性调用转换为 login(...)/用户属性调用;将超级属性调用转换为 setSuperProperties({...}),并放在原调用点(绝不可提前移位——源代码中 register 的位置决定了值何时可用,例如登录后;参见 mapping-framework.md §6)。

add 模式(双写):

  • 保留源调用,不做修改;紧邻其前后添加 AE track(...) 调用(遵循周围代码的风格)。
  • 保持两个 SDK 相互独立——两个方向都如此。 双写意味着对每个提供方/每个 SDK(Firebase、Amplitude、Sensors Data、Mixpanel、GA4 等)分别执行两次独立写入,而不是受源 SDK 状态控制的 AE 写入:
    • AE track()/login()/用户属性/setSuperProperties() 调用绝不能嵌套在源 SDK 的可用性检查、提前返回或错误处理路径中——例如放在 if (!analytics) return 之后、isSupported() 检查之后,或源代码的 try/catch 之内。如果源调用位于此类保护逻辑之后,应将 AE 调用移出,使其无条件执行。
    • 为每个 SDK 的写入分别设置自己的 try/catch:源 SDK 故障(不可用、isSupported() === false 或抛出错误)绝不能阻止 AE 写入;AE 故障也绝不能阻止源 SDK 写入(或导致其崩溃)。
    • 保护逻辑必须涵盖源 SDK 的全部故障情形,而非仅一种异常类型。已有的范围过窄的捕获(例如 Python 中针对 400 验证错误的 except ValueError,或 Java 中的 catch (IllegalArgumentException))不能作为独立性保护——其他故障(未配置 API 密钥导致 RuntimeError、网络/超时)仍会逃逸,并在 AE 写入前导致崩溃。应扩大捕获范围以涵盖所有异常,执行 AE 写入,然后重新抛出源异常,以保持原有的状态/错误语义(400/503)不变。
    • 每个 SDK 的初始化也要分别设置自己的 try/catch,以免某个 SDK 在加载时被卸载或禁用(初始化抛出错误/缺少模块)导致另一个 SDK 中断。无论哪个 SDK 被卸载或禁用,两个写入都必须继续正常工作。
    • 混合运行中的每个 add/add_after/add_before 条目也都适用上述要求。
  • 保留源 SDK 依赖;只添加 AE SDK 依赖。 异步调用点 — 源调用使用 await 等待(React Native / Flutter 中的 await analytics().logEvent(...),以及某些 await mixpanel.track(...))会返回 Promise,但 AE 客户端的 track() / login() / 用户属性 / setSuperProperties() 调用是同步的:
  • switch:移除被替换调用上的 await — 等待同步 AE 调用会触发 lint 警告,且不会改变任何行为。如果源调用位于 try/catch 中,且 catch 处理的是源 SDK 的 Promise 拒绝(网络重试、离线回退、错误报告),则该拒绝分支会随源 SDK 一并消失:移除现已无用的 catch 逻辑;如果 try 块中只剩 AE 调用,则将 try/catch 简化为直接调用 AE(AE 同步错误随后遵循代码技能的 Code Style:在 switch 模式下明确暴露)。绝不要为了保持结构一致,给 AE 调用套上虚构的 async / await。
  • add:原样保留源调用的 await 和 try/catch,并将 AE 调用放在该代码块之外,不使用 await,按上述独立性规则放入单独的 try/catch。

两种模式都遵循代码技能的插入规则:从 wiki 主文档获取 SDK 初始化信息(绝不猜测导入语句)、预先检查 git 状态、批量 Edit 并检查语言风格、添加 // @tracking <event_name> 注释前缀。

用户属性 append / unset(ae_call = user_append / user_unset):这两种操作不在计划的 update_type 枚举中(仅包含 user_set / user_setOnce / user_add),因此不会进入 draft.json — 在草稿中,其 update_type 写为 user_set(占位符)。在代码插入时,按映射中的 ae_call 直接输出 AE SDK 调用(user_append → userAppend,user_unset → userUnset),并从 wiki 主文档读取真实的方法签名(绝不猜测)。

**⚠️ add 模式覆盖代码技能中“Do NOT add try/catch”的 Code Style 规则。**该规则适用于单 SDK 场景(全新插入 / 切换完成后的 switch):只有一个 SDK 时,明确暴露错误才是目的,因此初始化 / 跟踪错误必须抛出,而不能被吞掉。双写则相反 — 上述“每个 SDK 各自使用 try/catch”规则同样适用于生成的 AE 代码:

  • AE 初始化必须采用相同的保护方式(使用独立的 try/catch / 可用性检查),确保 AE SDK 加载失败时绝不会在加载时抛出,也不会导致源 SDK 停止运行。
  • 绝不要输出加载时捕获 SDK、导致其引用永久为 undefined 并在每次调用时抛错的代码(例如包装器中的 var ta = window.TA;)— 缺少 AE SDK 时,每次调用都必须是空操作,而不能导致业务逻辑或源 SDK 写入崩溃。
  • switch 模式不适用此覆盖:切换完成后 AE 是唯一的 SDK,因此完全遵循代码技能的 Code Style(错误仍须明确暴露)。

阶段 5 — 验证

沿用代码技能的验证流程:

  1. ae-cli auth status / 确认当前主机;添加 / 选择调试设备。
  2. 触发迁移后的事件,并查询 ae-cli tracking debug-data list。
  3. 在 add 模式下,验证 AE 数据与仍在运行的源 SDK 数据;在 switch 模式下,确认源 SDK 已完全移除且 AE 数据正常流入。

模式

模式是否保留源 SDK?AE 调用位置依赖项变更
switch移除替换源调用移除源依赖,添加 AE 依赖
add保留添加在源调用旁边仅添加 AE 依赖

在阶段 2 询问用户要使用哪种模式。**默认使用 add(双写):在确认提示中将 add 作为推荐选项。**将 switch 视为非默认的破坏性选项——只有用户明确表示想要完全切换时才提供,且绝不要将其标记为推荐选项。

分阶段/混合运行:mode 是运行级默认值,但每个映射条目都有自己的 action(replace 与 add_after/add_before),并且以每个条目的 action 为准。switch 运行仍可将选定条目保留为 add_after,用于分阶段推出(例如,现在切换 identity + sign-up,同时在验证数据期间让 purchase 保持双写)。在这种情况下:替换后的文件会移除源 SDK,双写文件会保留源 SDK,且只有在完全切换(每个条目都是 replace)时才会移除源 SDK 依赖。请参阅 references/ir.md §mapping.json 和 mapping-framework.md §8。

⚠️ switch 具有破坏性(高风险):它会删除源跟踪调用、对应的导入以及源 SDK 依赖。默认情况下绝不要删除旧代码。删除任何内容前,必须明确告知用户将移除哪些内容(调用点 + 依赖项)并获得用户确认;在阶段 4 即将删除前还必须再次确认,即使用户已在阶段 2 选择 switch。

禁止事项

  • 猜测 AE SDK 导入/包名——始终通过 ae-generate-tracking-code 阅读 wiki 主文档(references/sdk-index.md)。
  • 将事件/属性名称修改为偏离 AE 方案的名称(snake_case、已注册名称)。
  • 将源 SDK 自动跟踪事件迁移为手动 track() 调用——应按适配器将其映射到 AE 自动跟踪开关,或直接舍弃。
  • 在 add 模式下,将 AE 的 track()/login()/用户属性调用嵌套在源 SDK 的可用性检查、提前返回或 try/catch 中;也不得让任一 SDK 的初始化不受保护,或使用范围过窄、仅捕获特定异常类型的代码保护源 SDK 写入,导致源 SDK 故障仍可能在 AE 写入前引发崩溃——对于每个提供方,双写都必须保持为两次独立写入(每个 SDK 各自使用 try/catch,包括初始化,并覆盖完整的故障范围),以确保一个 SDK 故障或被卸载时不会影响另一个。
  • 跳过阶段 2 的映射摘要确认。
  • 将 user_append/user_unset 写入 draft.json 的 update_type(方案枚举仅包含 user_set/user_setOnce/user_add)——追加/取消设置应保留在 mapping.json 的 ae_call 中,并在阶段 4 直接输出。
  • 在任何面向用户的输出中,将 “Super Property” 翻译为“超级属性”。
  • 在 git-status 预检查(插入模式)之前编写源代码或 AE 代码。
  • 编辑提供方适配器,将特定提供方逻辑加入核心代码——新增提供方时只能新增适配器文件。
  • 未在删除前立即明确警告用户并获得确认,就删除源跟踪代码或移除源 SDK 依赖(switch 模式)。

内部参考

  • references/ir.md — 规范化 IR 架构(scan.json/mapping.json)。
  • references/mapping-framework.md — 与提供方无关的映射算法。
  • references/ae-preset-properties.md — AE # 预置属性及系统字段的权威列表(只有这些 # 名称会被摄取)。
  • references/providers/README.md — 适配器约定及如何添加提供方。
  • references/providers/firebase.md — Firebase 适配器。
  • references/providers/amplitude.md — Amplitude 适配器。
  • references/providers/sensors-data.md — Sensors Data(神策)适配器。
  • references/providers/mixpanel.md — Mixpanel 适配器。
  • references/providers/ga4.md — Google Analytics 4(gtag.js/Measurement Protocol)适配器。

评论

支持 Markdown