> 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/start-guide/othertag/tag-shopify.md).

# Shopifyでタグ設置

> Shopifyアプリとの連携設定については[Shopify連携](/integrations/shopify.md)もご覧ください。

## 基本タグの設置

### 1. theme.liquidテンプレートを見つける

Shopify管理画面で「Online Store」→「Themes」を選択し、「…」ボタンから「Edit code」を選択

### 2. \<head>の下にptengineの基本タグを設置

下記画像右側の赤枠の部分に、Ptengineの基本タグを貼り付けてください。

#### **基本タグの一例**

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

{% hint style="danger" %}
**⚠️注意**

上記xxxxxxxxの部分はプロジェクトのIDであり、Ptengineのプロジェクトごとに異なります。

自社プロジェクトのIDと書き換えてください。

詳しくは[こちらの記事](https://ptmind-1.gitbook.io/ptengine-helpcenter/start-guide/quick/tag-setting)をご参照ください。
{% endhint %}

#### **Ptengineの基本タグの取得方法**

{% embed url="<https://app.arcade.software/share/9Co1auPt7xkZweCU7ehH>" %}

## イベントの設置

上記基本タグを設置するのみで、**購買完了、カートイン**などECサイトでよくある行動データの集計ができません。

そのため、Ptengine上でもイベントデータやCVデータを確認したい場合、イベントの設置もおすすめです。

### イベント収集タグの導入

#### 1、Shopify管理画面で「Settings」を選択

#### 2、「Customer events」を選択

#### 3、Pixel nameをつける

右上の黒いボタン「Add custom pixel」をクリックし、ポップアップの中でpixel nameを設定します。（下記図はサンプルとして「pt tracking on checkouts」という名前をつけております）

そしてAdd pixelをクリックしてください。

#### 4、保存

Codeに下記コードを貼り付けて、右上の「save」で保存します。

{% hint style="danger" %}
**⚠️注意**

**必ず**　`const sid`　**に自社PtengineのプロジェクトIDに置き換えてください！！！**
{% endhint %}

```js
// 貼り付け用コード
// 必ず sid をご自身のプロファイルIDに置き換えてください
const sid = "プロファイルidを入力してください";
const area = "jp";

// サンドボックスのパス形式は web pixel の runtimeContext によって決まる
const SANDBOX_RE =
  /\/(?:wpm|web-pixels)@[^/]+\/(?:app\/|custom\/)?web-pixel-[^/]+\/sandbox\/modern/;

let ptenginePromise = null;

/**
 * 実際のストアページ URL を取得する。
 * サンドボックス 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が既に存在する場合は即座にresolve
  if (window.ptengine) return Promise.resolve();
  // 読み込みが進行中であれば再利用し、二重に挿入しない
  if (ptenginePromise) return ptenginePromise;

  ptenginePromise = new Promise((resolve, reject) => {
    // 元の URL で送信する。SDK は setPVTag を ',' で分割し（replace 判定は厳密に第3要素を見る）、
    // 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 はドメイン不一致 / サンプリング / 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;
}

// 注文完了：注文単位の checkout_completed 1件 + 商品ごとに checkout_completed_order を1件ずつ送信
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（小計）を使用している。
  //    同一ファネル内で2種類の定義が混在すると金額を比較できないため、どちらに統一するか、
  //    または両側にもう一方のフィールドを追加するかをお客様と確認する必要がある。
  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：注文ごとに1回、注文情報をユーザープロファイルに書き込む。
      //    track より前に送信する必要がある。そうしないと後続イベントが識別済みユーザーに紐付かない。
      if (uid) {
        pt.identify(uid, {
          totalPrice: totalPrice,
          totalorderid: orderId,
        });
      }

      // 2) track：注文単位の checkout_completed を1回だけ送信（商品明細は含めない）
      pt.track("checkout_completed", eventProperties);

      // 3) 商品明細：商品ごとに checkout_completed_order を1件ずつ送信。
      //    注文単位イベントと紐付けられるよう 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 を1件ずつ送信するため、
  //    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 を1件ずつ送信するため、同様に膨らむ。
  //    改善方法は上記参照。
  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);
});
```

#### 5、公開

最後に右上の「Connect」ボタンを押してStep4で設置したコードを公開します。

### イベントタグの発火タイミングについて

<table><thead><tr><th width="318.41796875">イベント名</th><th>発火タイミング</th></tr></thead><tbody><tr><td>search_submitted</td><td>検索機能を使用した際</td></tr><tr><td>collection_viewed</td><td>一覧ページを閲覧した際</td></tr><tr><td>product_viewed</td><td>商品詳細ページを閲覧した際</td></tr><tr><td>product_added_to_cart</td><td>カートに商品を追加した際</td></tr><tr><td>product_removed_from_cart</td><td>カートから商品を削除した際</td></tr><tr><td>cart_viewed</td><td>カートページを閲覧した際</td></tr><tr><td>checkout_started</td><td>Checkoutページへ遷移した際</td></tr><tr><td>checkout_contact_info_submitted</td><td>連絡先（メールアドレス）を入力した際</td></tr><tr><td>checkout_address_info_submitted</td><td>住所情報を入力した際</td></tr><tr><td>checkout_shipping_info_submitted</td><td>配送方法を選択した際</td></tr><tr><td>payment_info_submitted</td><td>支払い方法を選択した際</td></tr><tr><td>checkout_completed</td><td>支払いが完了した際（注文単位・1注文につき1件）</td></tr><tr><td>checkout_completed_order</td><td>支払い完了時の商品明細（商品単位・1注文内の商品ごとに1件、totalorderid で注文単位イベントと紐付け）</td></tr></tbody></table>

## 注意事項

ShopifyでPtengineをご利用いただく際は、以下の点を必ずご確認ください。

### **1. Ptengineの基本タグは「Shopifyのテーマ」に直接設置してください**

Ptengineの基本タグは、Shopifyのテーマ（theme.liquid）へ直接設置してください。

Google Tag Manager経由での設置は、Shopifyの仕様上イベント計測ができない場合がございます。

そのため、GTMなどなどのタグマネージャーの経由での設置は推奨しておらず、サポート対象外となります。

### **2. ページ単位の送信制限や条件追加をしないでください**

Ptengine の基本スクリプトをアレンジし特定ページのみで発火させることやURL条件などで発火を制限することを推奨しておりません。

その状態で発生したデータ計測の不具合については、原因調査やサポート対応ができかねますのでご了承ください。

### **3. テーマを変更した場合は、再度タグ設置が必要です**

Shopifyでは、テーマを変更すると theme.liquid の内容がリセットされます。

そのため、テーマの切り替えや新しいテーマの適用を行った場合、

新しいテーマにもPtengineの基本タグを再度設置してください。

タグを再設置しない場合、Ptengineの計測は行われませんのでご注意ください。
