Skip to content

GA log list

Let's make a GA log list dialog that will be shown by stroke "galog".

js
const CONTROL_ID = 'ga-log';
const {
  receiver,
  restore,
} = _spy.intercept(CONTROL_ID);

const logTimeFormatter = new Intl.DateTimeFormat("en-US", {
  hour12: false,
  hour:   '2-digit',
  minute: '2-digit',
  second: '2-digit',
  fractionalSecondDigits: 3,
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});

const gaLogs = {};
const gaLogReader = new Proxy({}, {
  get(target, prop) {
    if (prop === "trackingIds") {
      return Array.from(Object.keys(gaLogs));
    }
    return (gaLogs[prop] ?? []).map(logItem => ({
      ...logItem,
      formattedTime: logTimeFormatter.format(new Date(logItem.time)),
      logStr: JSON.stringify(logItem.log),
    }));
  },
  set() {
    throw new Error("setter is not available");
  }
});

function addGaLog(log) {
  if (log.tid in gaLogs === false) {
    gaLogs[log.tid] = [];
  }
  gaLogs[log.tid].push({
    log: setupGaLog(log),
    time: Date.now(),
  });
}
function setupGaLog(log) {
  function replaceKey(key) {
    switch(key) {
      case "tid":
        return "trackingId";
      case "dt":
        return "title";
      case "en":
        return "eventName";
      default:
        break;
    }
    if (/^epn\./.test(key)) {
      return key.replace(/^epn\./, "");
    }
    return key;
  }
  return Object.fromEntries(
    Object.entries(log).map(([key,value]) => [replaceKey(key), value])
  );
}
function interpretGaLog(url, body) {
  return [
    () => Object.fromEntries(new URL(url).searchParams.entries()),
    () => Object.fromEntries(
      new URLSearchParams(decodeURIComponent(body)).entries(),
    ),
  ].reduce((acc, factory) => {
    const obj = (() => {
      try { return factory(); } catch { return {}; }
    })();
    return {
      ...acc,
      ...obj,
    };
  }, {});
}

function isUrlGa(url) {
  return /google/i.test(url) && /collect/i.test(url);
}

receiver.on(receiver.events.XHR_LOAD, (data) => {
  if(isUrlGa(data.url) === false) {
    return;
  }
  addGaLog(interpretGaLog(data.url, data.requestBody));
});

receiver.on(receiver.events.FETCH, (data) => {
  if(isUrlGa(data.input) === false) {
    return;
  }
  addGaLog(interpretGaLog(data.input, data.init.body));
});

receiver.on(receiver.events.BEACON, (data) => {
  if (isUrlGa(data.url) === false) {
    return;
  }
  addGaLog(interpretGaLog(data.url, data.data));
});

_spy.customElement.ensureTemplateView();
_spy.stroke.register("galog", () => {
  _spy.dialog.display((dialogElement) => {
    dialogElement.appendChild(
      document.createRange().createContextualFragment(`
        <template-view>
          <div class="wrapper">
            <ul class="tracking-id-list">
              <li :for="trackingIds">
                <button
                  type="button"
                  @click="selectTrackingId"
                  :text="."
                ></button>
              </li>
            </ul>
            <p class="title" :text="currentTrackingId"></p>
            <ul class="log-list">
              <li :for="currentTrackingLogRecords">
                <div class="log-item">
                  <div class="time" :text="formattedTime"></div>
                  <div class="log" :text="logStr" @click="copyLogToClipboard"></div>
                </div>
              </li>
            </ul>
          </div>
          <style>
          ul {
            list-style: none;
            padding: 0;
            margin: 0;
          }
          .wrapper {
            display: grid;
            gap: 16px;

            .title {
              text-align: center;
              margin: 0;
            }
            .tracking-id-list {
              display: flex;
              flex-direction: row;
              gap: 16px;

              button {
                cursor: pointer;
                padding: 8px 16px;
                border: none;
                box-shadow: 0 0 1px;
                color: inherit;
                background: transparent;
              }
            }
            .log-item {
              display: grid;
              grid-template-columns: auto 1fr;
              gap: 16px;
              .time {
                white-space: nowrap;
              }
              .log {
                word-break: break-all;
                text-align: left;
                cursor: pointer;
                position: relative;
              }
              .log.copied::after {
                content: "copied";
                position: absolute;
                top: 0;
                right: 0;
                color: yellow;
              }
            }
          }
          </style>
        </template-view>
      `)
    );
    const tvItem ={
      trackingIds: gaLogReader.trackingIds,
      currentTrackingId: "",
      currentTrackingLogRecords: [],
    };
    const tvElement = dialogElement.querySelector("template-view");
    tvElement.item = tvItem;
    tvElement.reducers = [
      (item) => {
        const currentTrackingId = item.currentTrackingId || item.trackingIds[0] || "";
        return {
          ...item,
          currentTrackingId,
          currentTrackingLogRecords: gaLogReader[currentTrackingId] ?? [],
        };
      },
    ];
    tvElement.eventHandlers = {
      selectTrackingId(e, item, wholeItem, reflux) {
        reflux({
          ...wholeItem,
          currentTrackingId: item,
        });
      },
      async copyLogToClipboard(e, item) {
        const target = e.target;
        await navigator.clipboard.writeText(item.logStr);
        target.classList.add("copied");
        await new Promise(resolve => setTimeout(resolve, 1000));
        target.classList.remove("copied");
      },
    };
  }, { title: "ga log" });
});