> For the complete documentation index, see [llms.txt](https://helps.ptengine.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://helps.ptengine.com/cn/start-guide/othertag/tag-shopify.md).

# Shopify 如何部署代码

> 关于Shopify应用集成配置，请参阅[Shopify集成](/cn/integrations/shopify.md)。

{% hint style="info" %}
推荐优先通过 Shopify 应用商店安装 **Ptengine for Shopify** 应用：安装后自动部署基础代码并采集关键行为事件，无需改动主题代码，详见[Ptengine for Shopify 应用安装](/cn/start-guide/othertag/tag-shopify-app.md)。

以下手动部署方式适用于无法安装应用，或需要在访客拒绝 Cookie 后仍采集数据的商店。
{% endhint %}

## Shopify电商埋码指南 <a href="#installing-tags-in-shopify" id="installing-tags-in-shopify"></a>

### 以下提供的脚本方便Shopify电商客户进行快速埋码设置 <a href="#quick-tag-setup-script" id="quick-tag-setup-script"></a>

#### 1. 代码中找到theme.liquid模板，设置基础埋码 <a href="#id-1.-find-the-theme.liquid-template" id="id-1.-find-the-theme.liquid-template"></a>

Tips: xxxxx位置[具体参考](/cn/start-guide/quick/tag-setup.md)![Alt](https://2044586328-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FnucbKsG9WGIoxCpzyoHL%2Fuploads%2Fgit-blob-36f39b8aa5ac7cde4338ccb7bf54dfd61a0caf0c%2Fstart-guide-tag-shopify-01.jpg?alt=media)

```
<!-- Ptengine Tag -->
<script src="https://js.ptengine.com/xxxxx.js"></script>
<!-- End Ptengine Tag -->
```

#### 2. 事件采集部署 <a href="#event-collection-setup" id="event-collection-setup"></a>

打开设置中的“客户事件”，将下列代码中的sid和步骤一一样，替换为sid\
\
![](https://2044586328-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FnucbKsG9WGIoxCpzyoHL%2Fuploads%2Fgit-blob-6a0eadbb4fcc15b2a517f840182b25e3a809e403%2Fstart-guide-tag-shopify-02.png?alt=media)\
\
并全部粘贴进自定义像素中。\
保存并连接。 请务必将id替换为您的sid

```
// 请务必将 sid 替换为您的档案 id
const sid = "请输入档案id";
const area = "com";

// 沙箱路径形态由 web pixel 的 runtimeContext 决定
const SANDBOX_RE =
  /\/(?:wpm|web-pixels)@[^/]+\/(?:app\/|custom\/)?web-pixel-[^/]+\/sandbox\/modern/;

let ptenginePromise = null;

/**
 * 取真实的店铺页面 URL。
 * 即带页面路径，剥掉沙箱段后可还原出真实页面 URL。但事件上下文更可靠，
 * 因此优先从 event.context 取，正则仅作回退。
 */
function getPageUrl(event) {
  return (
    event?.context?.document?.location?.href ||
    event?.context?.window?.location?.href ||
    window.location.href.replace(SANDBOX_RE, "")
  );
}

// sid / area 直接用模块常量，不再作为形参重复传递
function loadPtengineScript(pageUrl) {
  // 如果ptengine已经存在，则立即解决
  if (window.ptengine) return Promise.resolve();
  // 已有加载在飞行中，复用，不重复注入
  if (ptenginePromise) return ptenginePromise;

  ptenginePromise = new Promise((resolve, reject) => {
    // 使用原始url进行发包；SDK 按 ',' 解析 setPVTag（严格取第 3 段判定 replace），
    // URL 内的逗号会导致整段错位，必须转义
    window._pt_sp_2 = window._pt_sp_2 || [];
    window._pt_sp_2.push("setAccount," + sid);
    window._pt_sp_2.push(
      "setPVTag," + String(pageUrl).replace(/,/g, "%2C") + ",replace",
    );

    // 结构性限制：setPVTag 仅在首次加载时被 SDK 消费，因此同一沙箱生命周期内
    // 后续事件都沿用第一个事件的 URL 做页面归属。多页 Shopify 站每次导航都会重建
    // 沙箱，无影响；若主题是 SPA 式导航、或 checkout 单页走完多步，后续事件会挂在
    // 第一个 URL 上 —— 精确归属需依赖事件属性里的 url 字段。
    // 动态加载ptengine脚本
    const script = document.createElement("script");
    // 判断是否在checkout页面（必须用真实页面 URL，沙箱 URL 里不含 /checkouts/）
    const isCheckoutPage =
      /\/checkouts?\//.test(pageUrl) || /thank[_-]you/.test(pageUrl);
    const sandboxQuery = isCheckoutPage ? "" : "?sandbox"; // checkout页面不添加'?sandbox'
    script.src =
      "https://js.ptengine." + area + "/" + sid + ".js" + sandboxQuery;
    script.onload = () => resolve(); // 加载成功
    script.onerror = () => {
      ptenginePromise = null; // 允许后续事件重试
      reject(new Error("Script loading failed")); // 加载失败
    };
    document.head.appendChild(script);
  });

  return ptenginePromise;
}

/**
 * 加载完成后拿到可用的 ptengine 实例再执行 fn。
 * SDK 可能因 domain 不匹配 / 抽样 / URL 排除 / 重复布码被中断，此时 onload 已触发
 * 但 window.ptengine 不存在，必须显式告警，否则事件被静默丢弃。
 * @param {string}   pageUrl
 * @param {function} fn
 * @param {string}   [label] 事件名，出现问题时写进告警便于定位
 */
function withPtengine(pageUrl, fn, label) {
  return loadPtengineScript(pageUrl)
    .then(() => {
      if (!window.ptengine) {
        console.warn(
          "ptengine 未初始化（可能命中 URL 排除 / 抽样 / 重复布码），事件丢弃" +
            (label ? "：" + label : ""),
        );
        return;
      }
      fn(window.ptengine);
    })
    .catch((error) => {
      console.error(
        "ptengine脚本加载失败" + (label ? "，事件丢弃：" + label : ""),
        error,
      );
    });
}

function trackEvent(eventType, eventProperties, event) {
  const options = eventProperties || {};
  return withPtengine(
    getPageUrl(event),
    (pt) => pt.track(eventType, options),
    eventType,
  );
}

// 去除空值属性，避免发送 null/undefined/'' 字段
function removeEmptyKeys(obj) {
  for (let key in obj) {
    if (
      obj.hasOwnProperty(key) &&
      (obj[key] === null || obj[key] === undefined || obj[key] === "")
    ) {
      delete obj[key];
    }
  }
  return obj;
}

// 订单完成：1 条订单级 checkout_completed + 每个商品 1 条 checkout_completed_order
analytics.subscribe("checkout_completed", (event) => {
  const checkout = event?.data?.checkout;
  if (!checkout) return;

  // 从订单客户 ID（gid://shopify/Customer/xxx）中提取数字 ID 作为 uid
  const rawCustomerId = checkout?.order?.customer?.id;
  const numericCustomerId = rawCustomerId ? rawCustomerId.split("/").pop(): null;
  const uid = numericCustomerId;
  // ⚠️ 口径待定：此处用 totalPrice（含税费运费的成交总额），而 checkout_started
  //    与 payment_info_submitted 用的是 subtotalPrice（小计）。同一漏斗里两种口径
  //    金额不可比，需与客户确认统一到哪一个，或在两端都补上另一个字段。
  const totalPrice = checkout?.totalPrice?.amount;
  const currencyCode = checkout?.totalPrice?.currencyCode;
  const orderId = checkout?.order?.id;
  const items = checkout?.lineItems || [];

  const eventProperties = removeEmptyKeys({
    totalPrice: totalPrice,
    currencyCode: currencyCode,
    totalorderid: orderId,
    productCount: items.length,
  });

  withPtengine(
    getPageUrl(event),
    (pt) => {
      // 1) identify：整单一次，把订单信息写到用户档案。
      //    必须在 track 之前发，否则后面的事件归不到已识别用户身上。
      if (uid) {
        pt.identify(uid, {
          totalPrice: totalPrice,
          totalorderid: orderId,
        });
      }

      // 2) track：只发一次订单级 checkout_completed（不含商品明细）
      pt.track("checkout_completed", eventProperties);

      // 3) 商品明细：每个商品单独发 1 条 checkout_completed_order，
      //    带 totalorderid 以便与订单级事件关联
      items.forEach((item) => {
        const itemProperties = removeEmptyKeys({
          totalorderid: orderId,
          sku: item?.variant?.sku,
          name: item?.title,
          spu: item?.variant?.product?.type,
          // ⚠️ 口径待定：item.id 是“行项 ID”，每单都不同，拿它做商品维度聚合会完全散开。
          //    商品维度应改用 item.variant.product.id，规格维度用 item.variant.id。
          //    改字段含义会影响已有报表，需客户确认后再动。
          id: item?.id,
          quantity: item?.quantity,
          price: item?.finalLinePrice?.amount,
          currencyCode: currencyCode,
        });
        pt.track("checkout_completed_order", itemProperties);
      });
    },
    "checkout_completed",
  );
});

analytics.subscribe("search_submitted", (event) => {
  const eventProperties = removeEmptyKeys({
    keyword: event?.data?.searchResult?.query,
    resultCount: event?.data?.searchResult?.productVariants?.length,
  });
  trackEvent("search_submitted", eventProperties, event);
});

analytics.subscribe("collection_viewed", (event) => {
  const eventProperties = removeEmptyKeys({
    id: event?.data?.collection?.id,
    name: event?.data?.collection?.title,
    productCount: event?.data?.collection?.productVariants?.length,
  });
  trackEvent("collection_viewed", eventProperties, event);
});

analytics.subscribe("product_viewed", (event) => {
  const origin = event?.context?.window?.origin || window.location.origin;
  const variant = event?.data?.productVariant;
  const eventProperties = removeEmptyKeys({
    sku: variant?.sku,
    name: variant?.product?.title,
    // ⚠️ 口径待定：type 与 spu 取的是同一个值，重复占用事件属性配额且语义不清。
    //    保留是因为可能已有报表在用其中之一，需客户确认后再合并。
    type: variant?.product?.type,
    spu: variant?.product?.type,
    price: variant?.price?.amount,
    currencyCode: variant?.price?.currencyCode,
    url: variant?.product?.url ? origin + variant.product.url : undefined,
    imageUrl: variant?.image?.src,
  });
  trackEvent("product_viewed", eventProperties, event);
});

analytics.subscribe("product_added_to_cart", (event) => {
  const origin = event?.context?.window?.origin || window.location.origin;
  const merchandise = event?.data?.cartLine?.merchandise;
  const eventProperties = removeEmptyKeys({
    url: merchandise?.product?.url ? origin + merchandise.product.url: undefined,
    name: merchandise?.product?.title,
    sku: merchandise?.sku,
    quantity: event?.data?.cartLine?.quantity,
    price: merchandise?.price?.amount,
    currencyCode: merchandise?.price?.currencyCode,
    spu: merchandise?.product?.type,
  });
  trackEvent("product_added_to_cart", eventProperties, event);
});

analytics.subscribe("product_removed_from_cart", (event) => {
  const origin = event?.context?.window?.origin || window.location.origin;
  const merchandise = event?.data?.cartLine?.merchandise;
  const eventProperties = removeEmptyKeys({
    url: merchandise?.product?.url ? origin + merchandise.product.url : undefined,
    name: merchandise?.product?.title,
    sku: merchandise?.sku,
    quantity: event?.data?.cartLine?.quantity,
    price: merchandise?.price?.amount,
    currencyCode: merchandise?.price?.currencyCode,
    spu: merchandise?.product?.type,
  });
  trackEvent("product_removed_from_cart", eventProperties, event);
});

analytics.subscribe("cart_viewed", (event) => {
  const cart = event?.data?.cart;
  if (!cart) {
    trackEvent("cart_viewed", {}, event);
    return;
  }
  const origin = event?.context?.window?.origin || window.location.origin;
  const lines = cart?.lines || [];

  // ⚠️ 口径提醒：此处对购物车里每个商品各发一条 cart_viewed，
  //    3 件商品就是 3 条同名事件 —— 事件配额成倍消耗，转化率分母也会膨胀。
  //    保留此行为以免打断已有报表；若要改成“1 条购物车级 + N 条商品级”，
  //    那是口径变更，需先与客户确认并同步调整报表。
  if (lines.length) {
    lines.forEach((line) => {
      const merchandise = line?.merchandise;
      const eventProperties = removeEmptyKeys({
        totalQuantity: cart?.totalQuantity,
        url: merchandise?.product?.url ? origin + merchandise.product.url : undefined,
        name: merchandise?.product?.title,
        sku: merchandise?.sku,
        spu: merchandise?.product?.type,
        quantity: line?.quantity,
        // 有意选择：line.cost.totalAmount 是行小计，merchandise.price 是单价，
        // 两者语义不同，不做兜底混填 —— cost 缺失时宁可丢字段，也不让 price 含义不明。
        price: line?.cost?.totalAmount?.amount,
        currencyCode: line?.cost?.totalAmount?.currencyCode,
      });
      trackEvent("cart_viewed", eventProperties, event);
    });
  } else {
    trackEvent(
      "cart_viewed",
      removeEmptyKeys({ totalQuantity: cart?.totalQuantity }),
      event,
    );
  }
});

analytics.subscribe("checkout_started", (event) => {
  const checkout = event?.data?.checkout;
  const origin = event?.context?.window?.origin || window.location.origin;
  const checkoutItems = checkout?.lineItems;
  const totalPrice = checkout?.subtotalPrice?.amount;
  const currencyCode = checkout?.subtotalPrice?.currencyCode;

  // ⚠️ 同 cart_viewed：此处每个商品发一条 checkout_started，同样会膨胀。改法见上。
  if (checkoutItems && checkoutItems.length) {
    checkoutItems.forEach((item) => {
      const eventProperties = removeEmptyKeys({
        totalPrice: totalPrice,
        currencyCode: currencyCode,
        url: item?.variant?.product?.url  ? origin + item.variant.product.url : undefined,
        name: item?.variant?.product?.title,
        sku: item?.variant?.sku,
        spu: item?.variant?.product?.type,
        quantity: item?.quantity,
        price: item?.finalLinePrice?.amount,
      });
      trackEvent("checkout_started", eventProperties, event);
    });
  } else {
    trackEvent(
      "checkout_started",
      removeEmptyKeys({ totalPrice, currencyCode }),
      event,
    );
  }
});

analytics.subscribe("checkout_contact_info_submitted", (event) => {
  trackEvent("checkout_contact_info_submitted", {}, event);
});

analytics.subscribe("checkout_address_info_submitted", (event) => {
  trackEvent("checkout_address_info_submitted", {}, event);
});

analytics.subscribe("checkout_shipping_info_submitted", (event) => {
  trackEvent("checkout_shipping_info_submitted", {}, event);
});

analytics.subscribe("payment_info_submitted", (event) => {
  const checkout = event?.data?.checkout;
  const eventProperties = removeEmptyKeys({
    totalPrice: checkout?.subtotalPrice?.amount,
    currencyCode: checkout?.subtotalPrice?.currencyCode,
  });
  trackEvent("payment_info_submitted", eventProperties, event);
});
```

<br>

#### 3.事件定义： <a href="#event-definition" id="event-definition"></a>

search\_submitted:使用搜索功能

collection\_viewed：列表页访问

product\_viewed：商品详情页访问

product\_added\_to\_cart：添加购物车

product\_removed\_from\_cart：移除购物车

cart\_viewed：购物车页面访问

checkout\_started：进入checkout页面

checkout\_contact\_info\_submitted：输入联系方式：邮箱

checkout\_address\_info\_submitted：输入地址

checkout\_shipping\_info\_submitted：选择快递信息

payment\_info\_submitted：选择支付方式

checkout\_completed：支付成功（订单级，一单一条）

checkout\_completed\_order：支付成功的商品明细（商品级，一单内每个商品一条，通过totalorderid与订单级事件关联）
