> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google Tag Manager 设置

> 使用 Google Tag Manager (GTM) 将 OneSignal Web Push 添加到您的网站，包括 service worker 设置、初始化和安全设置标签。

本指南展示如何使用 Google Tag Manager (GTM) 加载和初始化 OneSignal Web SDK，然后在初始化后可选地设置 External ID 和 OneSignal 标签。

## 前提条件

* 支持 HTTPS 的网站。
* 您可以在 GTM 中为网站的容器发布更改。
* 您已完成 OneSignal [Web SDK 设置](./web-sdk-setup)流程，直到**将代码添加到网站**步骤。这将为您提供：
  * OneSignal Web Push 应用和 App ID。
  * [OneSignal Service Worker](./onesignal-service-worker) 设置。

## 设置

### 1. 设置您的 OneSignal Web 应用

按照 [Web SDK 设置](./web-sdk-setup)直到**将代码添加到网站**步骤。这是您获取 OneSignal App ID 的地方。

<Frame caption="到达此步骤后，您需要对代码进行一些调整以便与 Google Tag Manager 配合使用。">
  <img src="https://mintcdn.com/onesignal/Y9PryqrHCRmPv_BC/images/web-push/add-code-to-site.png?fit=max&auto=format&n=Y9PryqrHCRmPv_BC&q=85&s=445d73fafe6bd92c5a6dcfbac563c1ee" alt="OneSignal Web SDK 设置面板中的将代码添加到网站步骤" width="2588" height="1638" data-path="images/web-push/add-code-to-site.png" />
</Frame>

<Warning>
  您必须将 OneSignal Service Worker 文件直接上传到您的服务器。请参见 [OneSignal Service Worker](./onesignal-service-worker)。
</Warning>

### 2. 创建 GTM 变量

为您在标签中引用的值创建 GTM 变量。这可以避免硬编码，并使您的设置更易于维护。

**创建 `ONESIGNAL_APP_ID` 变量**

1. 在 GTM 中，转到 **Variables > New**。
2. 选择 **Constant**。
3. 将其命名为 `ONESIGNAL_APP_ID`
4. 将值设置为您的 OneSignal App ID。
5. 保存

<Frame caption="创建 OneSignal App ID 变量">
  <img src="https://mintcdn.com/onesignal/wJS3gHTEqDzyW0IP/images/web-push/gtm-app-id-variable.png?fit=max&auto=format&n=wJS3gHTEqDzyW0IP&q=85&s=8a7752f1d4095d4c8e6b8119a9de2bfc" alt="在 Google Tag Manager 中创建 OneSignal App ID 变量" width="2378" height="1656" data-path="images/web-push/gtm-app-id-variable.png" />
</Frame>

<Check>
  现在您可以在 GTM 的任何地方使用 `{{ ONESIGNAL_APP_ID }}` 引用您的 App ID。
</Check>

**创建 `ONESIGNAL_EXTERNAL_ID` 变量（推荐）**

如果您使用外部标识符关联用户（例如，来自数据库或身份验证系统的用户 ID），请使用此变量。

根据值在您网站上的可用方式选择变量类型。常用选项：

* Data Layer Variable（推荐）
* First-Party Cookie
* DOM Variable（高级）

### 3. 创建 OneSignal init 标签

1. 在 GTM 中，转到 **Tags > New**
2. 为标签命名：`OneSignal - Init`
3. Tag Type：**Custom HTML**
4. 粘贴以下代码。
5. 在 **Advanced Settings > Tag firing options** 下，设置 **Once per page**。
6. 在 **Triggering** 下，选择 **Initialization - All Pages**。

```html HTML theme={null}
<!--
  OneSignal – 使用 Google Tag Manager 初始化 Web SDK

  此代码片段：
  - 加载 OneSignal Web SDK
  - 使用您的 App ID 初始化 OneSignal
  - 启用订阅铃铛 (notifyButton)

  适用于大多数网站。
-->

<!-- 1. 加载 OneSignal Web SDK (v16) -->
<!-- 此脚本必须在您希望 OneSignal 可用的每个页面上加载 -->
<script
  src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js"
  defer>
</script>

<script>
  // 确保 GTM dataLayer 存在
  // 此处仅用于可选地推送 "OneSignalInitialized" 事件
  window.dataLayer = window.dataLayer || [];

  // OneSignalDeferred 是一个队列，在 SDK 完全加载后运行
  window.OneSignalDeferred = window.OneSignalDeferred || [];

  // 2. SDK 就绪后初始化 OneSignal
  window.OneSignalDeferred.push(function (OneSignal) {

    OneSignal.init({
      /*
        必填
        建议将 OneSignal App ID 设置为 GTM 变量。
        您可以在 OneSignal 控制面板中找到：
        Settings > Keys & IDs
      */
      appId: "{{ONESIGNAL_APP_ID}}",

      /*
        可选 – 仅在您的 SERVICE WORKER 不在根目录时需要

        如果您的 service worker 位于：
          /OneSignalSDKWorker.js

        …则不应设置 serviceWorkerPath 或 serviceWorkerParam。

        仅在您的 service worker 位于子目录中时取消注释并更新以下选项
        （例如：/push/onesignal/）。
      */

      //serviceWorkerPath: "push/onesignal/OneSignalSDKWorker.js",
      //serviceWorkerParam: { scope: "/push/onesignal/" },

      /*
        可选
        启用 OneSignal 订阅铃铛 (notifyButton)，
        允许用户订阅或取消订阅通知。
        更多提示选项请参见：https://documentation.onesignal.com/docs/en/permission-requests
      */
      notifyButton: {
        enable: true
      }
    })
    .then(function () {
      // OneSignal 初始化成功
      console.log("[OneSignal] init success");

      // 推荐：推送事件到 GTM 以触发其他标签
      window.dataLayer.push({
        event: "OneSignalInitialized"
      });
    })
    .catch(function (e) {
      // 初始化失败（无效 App ID、缺少 service worker 等）
      console.log("[OneSignal] init failed", e);
    });
  });
</script>
```

<Frame caption="配置 OneSignal - Init 标签">
  <img src="https://mintcdn.com/onesignal/Y9PryqrHCRmPv_BC/images/web-push/gtm-init-tag.png?fit=max&auto=format&n=Y9PryqrHCRmPv_BC&q=85&s=6cec423472cb84777bddd893a59c83ff" alt="在 Google Tag Manager 中配置 OneSignal - Init 标签" width="2442" height="2296" data-path="images/web-push/gtm-init-tag.png" />
</Frame>

<Warning>
  如果您使用同意横幅 / CMP，请参阅下面的 [Consent Mode 和隐私注意事项](#consent-mode-and-privacy-considerations)选项。
</Warning>

### 4. 设置 External ID 和标签

设置 [External ID](./users#external-id) 是可选的，但建议设置，因为它允许您跨设备识别用户并与您的后端同步。

**将 `ONESIGNAL_EXTERNAL_ID` 推送到 dataLayer**

此示例展示如何将用户 ID 推送到 dataLayer，以便 GTM 可以通过 `ONESIGNAL_EXTERNAL_ID` 变量（在步骤 2 中创建）读取它。

```html HTML theme={null}
<script>
  window.dataLayer = window.dataLayer || [];

  // 从数据库或身份验证系统获取您的用户 ID。
  // 确保这是一个字符串值。
  var userId = "your_user_id_here";

  dataLayer.push({
    ONESIGNAL_EXTERNAL_ID: String(userId),
  });
</script>
```

**创建 GTM 标签以设置 External ID**
标签配置：

* 标签名称：`OneSignal – Set External ID`
* 标签类型：**Custom HTML**
* Tag firing options：**Once per page**
* Trigger：
  * 创建 `OneSignalInitialized` 的自定义事件触发器（在上面的 **OneSignal - Init** 标签中设置），并且
  * 可选地，如果您知道用户 ID 在页面加载时可用。

<Warning>
  设置 External ID 的必需方法是 `OneSignal.login(externalId)`，其中 `externalId` 是一个字符串。

  如果 `{{ONESIGNAL_EXTERNAL_ID}}` 为空（或 GTM 替换为 "undefined" / "null"），login 调用将被跳过，External ID 将不会被设置。这是一个常见的 GTM 时序问题。
</Warning>

<CodeGroup>
  ```html 设置 External ID 的基本示例 theme={null}
  <script>
    // OneSignalDeferred 确保在 OneSignal SDK 就绪后运行
    window.OneSignalDeferred = window.OneSignalDeferred || [];

    window.OneSignalDeferred.push(function (OneSignal) {
      /*
        从 Google Tag Manager 读取 External ID。
        这应该是一个 GTM 变量（Data Layer Variable 或 Custom JS Variable）。
      */
      var externalId = "{{ONESIGNAL_EXTERNAL_ID}}";

      console.log("[OneSignal] raw external ID from GTM:", externalId);

      /*
        基本验证：
        - GTM 可能将 undefined/null 替换为字符串
        - OneSignal.login 需要一个字符串
      */
      if (!externalId || externalId === "undefined" || externalId === "null") {
        console.log("[OneSignal] External ID missing, skipping login");
        return;
      }

      // 确保 External ID 是一个干净的字符串
      externalId = String(externalId).trim();

      console.log("[OneSignal] Calling OneSignal.login with External ID:", externalId);

      /*
        使用 External ID 将用户登录到 OneSignal。
        这将当前浏览器/设备链接到此用户。
      */
      OneSignal.login(externalId)
        .then(function () {
          console.log("[OneSignal] External ID set successfully:", externalId);
        })
        .catch(function (e) {
          console.log("[OneSignal] Failed to set External ID", e);
        });
    });
  </script>
  ```

  ```html 如果 External ID 未设置，请尝试使用的高级示例 theme={null}
  <script>
    window.dataLayer = window.dataLayer || [];
    window.OneSignalDeferred = window.OneSignalDeferred || [];

    OneSignalDeferred.push(function (OneSignal) {
      var rawExternalId = "{{ONESIGNAL_EXTERNAL_ID}}";

      // ---- Helpers ----
      function log() {
        console.log.apply(console, ["[OneSignal External ID]"].concat([].slice.call(arguments)));
      }

      function normalizeExternalId(v) {
        // GTM commonly substitutes these as strings
        if (
          v === undefined ||
          v === null ||
          v === "undefined" ||
          v === "null"
        ) return null;

        var s = String(v).trim();
        if (!s.length) return null;

        return s;
      }

      function pushDL(eventName, extra) {
        try {
          var payload = Object.assign({ event: eventName }, extra || {});
          window.dataLayer.push(payload);
        } catch (e) {
          // no-op
        }
      }

      function readStateSnapshot() {
        var snapshot = {
          onesignalId: null,
          externalId: null,
          pushSubscriptionId: null
        };

        try {
          snapshot.onesignalId = OneSignal.User && OneSignal.User.onesignalId;
          snapshot.externalId = OneSignal.User && OneSignal.User.externalId;
          snapshot.pushSubscriptionId =
            OneSignal.User &&
            OneSignal.User.PushSubscription &&
            OneSignal.User.PushSubscription.id;
        } catch (e) {
          log("Error reading OneSignal.User state", e);
        }

        return snapshot;
      }

      function isExternalIdApplied(targetExternalId) {
        var current = normalizeExternalId(OneSignal.User && OneSignal.User.externalId);
        return current === targetExternalId;
      }

      // ---- Initial logging ----
      log("Tag fired. rawExternalId:", rawExternalId, "type:", typeof rawExternalId);

      var externalId = normalizeExternalId(rawExternalId);
      log("Normalized externalId:", externalId, "type:", typeof externalId);

      if (!externalId) {
        log("Not calling login(): externalId missing/invalid");
        pushDL("OneSignalExternalIdMissing", { reason: "invalid_or_missing_external_id" });
        return;
      }

      // Optional: enable verbose OneSignal logs during testing
      if (OneSignal.Debug && OneSignal.Debug.setLogLevel) {
        OneSignal.Debug.setLogLevel("trace");
        log("Enabled OneSignal Debug log level: trace");
      }

      // ---- Attach User State observer ----
      var changeFired = false;

      OneSignal.User.addEventListener("change", function (event) {
        changeFired = true;

        log("User change event fired:", event);

        var snapshot = readStateSnapshot();
        log("User state snapshot:", snapshot);

        // Helpful: push snapshot-ish DL event (optional)
        pushDL("OneSignalUserStateChanged", {
          onesignal_id: snapshot.onesignalId || "",
          external_id: normalizeExternalId(snapshot.externalId) || "",
          push_subscription_id: snapshot.pushSubscriptionId || ""
        });
      });

      // ---- Login + confirm + retry ----
      var attempt = 0;
      var MAX_RETRIES = 3;
      var CONFIRM_WINDOW_MS = 1500;
      var BASE_BACKOFF_MS = 500;

      function doLogin() {
        attempt += 1;
        changeFired = false;

        log("Calling OneSignal.login()", { externalId: externalId, attempt: attempt });

        OneSignal.login(externalId)
          .then(function () {
            log("OneSignal.login() promise resolved");
            waitForConfirmation();
          })
          .catch(function (e) {
            log("OneSignal.login() promise rejected", e);
            retry("promise_rejected");
          });
      }

      function waitForConfirmation() {
        var start = Date.now();

        (function check() {
          if (isExternalIdApplied(externalId)) {
            log("Confirmed externalId applied via state check:", externalId);

            var snapshot = readStateSnapshot();
            log("Final state snapshot:", snapshot);

            pushDL("OneSignalExternalIdSet", {
              external_id: externalId,
              attempt: attempt,
              push_subscription_id: snapshot.pushSubscriptionId || ""
            });

            return;
          }

          if (changeFired) {
            log("Change event observed but externalId not yet reflected; waiting...");
          }

          if (Date.now() - start >= CONFIRM_WINDOW_MS) {
            log("No confirmation within window", {
              attempt: attempt,
              changeFired: changeFired,
              currentExternalId: normalizeExternalId(OneSignal.User && OneSignal.User.externalId)
            });

            retry("no_confirmation");
            return;
          }

          setTimeout(check, 100);
        })();
      }

      function retry(reason) {
        if (attempt >= MAX_RETRIES) {
          log("Giving up after max retries", { attempts: attempt, reason: reason });

          var snapshot = readStateSnapshot();
          log("State at give-up:", snapshot);

          pushDL("OneSignalExternalIdSetFailed", {
            external_id: externalId,
            reason: reason,
            attempts: attempt,
            push_subscription_id: snapshot.pushSubscriptionId || ""
          });

          return;
        }

        var delay = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
        log("Retrying login after delay", { delayMs: delay, reason: reason, nextAttempt: attempt + 1 });

        setTimeout(doLogin, delay);
      }

      // If already applied, don't spam login()
      if (isExternalIdApplied(externalId)) {
        log("ExternalId already applied; skipping login.", externalId);

        var snapshot = readStateSnapshot();
        log("Current state snapshot:", snapshot);

        pushDL("OneSignalExternalIdAlreadySet", {
          external_id: externalId,
          push_subscription_id: snapshot.pushSubscriptionId || ""
        });

        return;
      }

      // Kick it off
      doLogin();
    });
  </script>
  ```
</CodeGroup>

#### 设置标签

此步骤使用 Web SDK 发送 [OneSignal 标签](./add-user-data-tags)。

标签配置：

* 名称：`OneSignal - Add Tags`
* Tag Type：**Custom HTML**
* Tag firing options：**Once per page**
* Trigger：
  * `OneSignalInitialized`，以及
  * 您的标签数据可用条件（例如：登录后、个人资料页面、购买后）。

粘贴此代码并替换示例标签 TAG 和 VALUE。

```html HTML theme={null}
<script>
  window.OneSignalDeferred = window.OneSignalDeferred || [];
  window.OneSignalDeferred.push(function (OneSignal) {
    OneSignal.User.addTags({
      TAG_1: "VALUE_1",
      TAG_2: "VALUE_2",
    });
  });
</script>
```

<Note> 仅在有用户数据可用时发送标签（例如，登录后、加载个人资料后或已知转化事件后）。</Note>

### Consent Mode 和隐私注意事项

如果您的网站使用 Consent Mode / CMP，请决定 OneSignal 应何时加载：

* 仅在同意后（欧盟/英国常见），或
* 立即（在默认允许"功能性"存储的地方常见）。

GTM 支持 Consent Initialization 触发器和标签级同意控制，以根据用户同意管理标签行为。但是，OneSignal 也提供隐私同意方法来控制 SDK 何时加载。

* [处理个人数据](./handling-personal-data)
* [Web SDK 隐私方法](./web-sdk-reference#privacy)

***

## 测试

1. 在 GTM 中，打开 Preview 模式。
2. 加载您的网站并确认：
   * `OneSignal - Init` 触发一次。
   * `OneSignalInitialized` 出现在 GTM 事件时间线中（如果您保留了事件推送）。
3. 订阅您的网站。有关提示详细信息，请参阅[网页权限提示](./permission-requests)。
4. 在 OneSignal 控制面板中，转到 **Audience > Subscriptions** 并确认：
   * 选择加入后出现订阅。
   * 如果设置了 External ID，则可见 External ID。
5. 从 **Messages > New Push** 发送测试推送。

<Check> 如果初始化正常工作，您将看到选择加入后 OneSignal 中出现订阅。</Check>

### 故障排除

* Init 标签触发，但 SDK 从未加载
  * 检查是否有 Content Security Policy (CSP) 阻止 `https://cdn.onesignal.com`。
  * 检查广告拦截器/脚本拦截器。

* `dataLayer` 错误
  * 确保在任何 `dataLayer.push()` 调用之前设置 `window.dataLayer = window.dataLayer || []`。

* 重复提示 / 重复 SDK 加载
  * 确保您没有通过网站代码、CMS 插件或另一个 GTM 标签加载 OneSignal。

* Add Tags 运行但未出现在 OneSignal 中
  * 确认 Trigger Group 等待 `OneSignalInitialized`。
  * 确认您的用户操作触发器实际触发。
  * 确认标签是有效的键/值对，并在[计划限制](https://onesignal.com/pricing)内。

<Warning>
  如果您仍需要帮助，请参阅 [Web SDK 故障排除](./troubleshooting-web-push)以获取常见修复方法。
</Warning>

## 后续步骤

* [网页权限提示](./permission-requests)
* [标签](./add-user-data-tags)
* [Web SDK 参考](./web-sdk-reference)

***
