Article

無料ツールで構築するマーケティング基盤

高田晃太郎
無料ツールで構築するマーケティング基盤

Chiefmartecの2023年版マーケティングテクノロジー・ランドスケープには11,038のソリューションが掲載されました¹。選択肢が爆発的に増える一方で、導入・解約・移行のたびにデータが分断され、学習コストとSaaS費用が雪だるま式に膨らむ現実は見過ごされがちです。公開情報と一般的なTCO試算を踏まえると、イベント計測からデータ保管、モデリング、可視化、メールや広告へのアクティベーションまで、主要機能の大半はOSSと各社の無償枠を組み合わせるだけで安定運用できるケースは少なくありません。鍵は、ツール中心ではなくデータ中心に設計し、切り替えに強いイベントスキーマ(イベント項目の取り決め)とサーバーサイド計測を最初から据えることです。CTOやエンジニアリーダーに向け、本稿では実装コードと運用設計を一体で提示します。

設計原則と全体アーキテクチャ

最初に決めるべきはツールではなく境界です。ユーザーの同意と識別を司るレイヤー、イベント収集のレイヤー、ストレージと変換のレイヤー、可視化と意思決定のレイヤー、そしてアクティベーションのレイヤーを明確に分割します。レイヤー間の契約はイベントスキーマとデータマートの定義で表現し、どのコンポーネントも等価交換できるように作ると、無償枠の乗り換えが容易になります。

ベンダーロックインを避ける設計として、行動データの主権を自社に置くことを最優先にします。計測はクライアントSDK任せにせず、まず自社のサーバーエンドポイントに集約し、そこでスキーマ検証と匿名化を行ってからストレージに書き込みます。広告プラットフォームやMAへの送信はその後に複製します。これにより同意撤回や削除要求にも一貫して応えられ、計測ロスを抑えられます。

サーバーサイド計測とプライバシーは両立します。ユーザーのIPやUser-Agentはハッシュ化(HMACなど)し、同意の有無に応じて粒度を変えるルールをコードで明示します。Cookieは最小限にし、FPIDやUUIDはサーバー生成に寄せます。ブラウザ発のfetchが失敗しても送信を試みるよう、Beacon API(ページ離脱時も送信しやすいAPI)や再送の仕組みを持たせてドロップ率を抑えます。サーバーサイド計測の有効性は各種技術解説でも整理されており、クライアント依存を減らしながら計測品質とプライバシー配慮を両立させる実践が紹介されています¹⁰。また、IPやUser-Agentなどのメタデータは組み合わせ次第で識別に寄与し得るため、匿名化やハッシュ化といった保護措置は重要です¹¹。

低コストツールの組み合わせで描く基盤

収集はGTMのタグ配信を補助に使いながら、メインは自社エンドポイントに集約します。エッジでの受け口はCloudflare WorkersやVercel Serverlessの無償枠を活用し²³、ストレージはNeonのPostgreSQLやSelf-hostedのClickHouseが有力です。変換はdbt Core(OSS)でモデル化し⁴、Airbyte OSSがあればELT(取り込み後に変換)の拡張性を確保できます⁶。可視化はMetabase OSSやLooker Studioの接続機能で、現場運用は十分回ります⁵。アクティベーションはHubSpotのFreeプランのフォーム・CRM⁸、あるいはBrevoのフリープランによるメール配信⁷を用いれば、初期段階から費用を抑えて施策の反復が可能です。Consentはオープンソースのcookieconsentライブラリで同意UIを提供し、同意トグルをイベントに同伴させます⁹。

この構成の利点は、どの層も小さく始め、必要に応じて同等機能の商用版に置き換えられる点です。例えばストレージはPostgreSQLからBigQueryのSandboxへ、可視化はMetabaseから有償のBIへ移るとしても、dbtで定義されたモデルやイベントスキーマは再利用できます。TCOの観点でも、ライセンス費を抑え、インフラ費は各社の無償枠を活用することで初期コストを抑制し、人件費に集中投下できます。さらに、Airbyte・dbt・データウェアハウス・BIというモダンデータスタックの組み合わせは、小規模から段階的にスケールさせやすい構成としても広く紹介されています⁶。

実装ガイド:計測からアクティベーションまで

フロントエンドのイベント送信(再送とBeacon対応)

// /public/analytics.js
(function () {
  const ENDPOINT = 'https://your-domain.vercel.app/api/events';
  const RETRIES = 3;
  async function send(payload) {
    const body = JSON.stringify(payload);
    if (navigator.sendBeacon) {
      const ok = navigator.sendBeacon(ENDPOINT, body);
      if (ok) return true;
    }
    let attempt = 0;
    while (attempt < RETRIES) {
      try {
        const res = await fetch(ENDPOINT, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body,
          keepalive: true,
          credentials: 'omit'
        });
        if (res.ok) return true;
      } catch (e) {}
      await new Promise(r => setTimeout(r, Math.pow(2, attempt++) * 200));
    }
    return false;
  }
  function track(event, props = {}) {
    const base = {
      event,
      occurred_at: new Date().toISOString(),
      url: location.href,
      referrer: document.referrer || null,
      ua: navigator.userAgent,
      consent: window.__consent || { ad: false, analytics: true }
    };
    return send({ ...base, ...props });
  }
  window.analytics = { track };
})();

このスニペットはBeacon APIを優先し、失敗時は指数バックオフでfetchを再試行します。サーバーで匿名化する方針のため、クライアントでは個人特定情報を扱いません。

サーバーエンドポイント(Vercel Serverless + PostgreSQL/Neon)

// /api/events.js (Vercel, Node.js)
import crypto from 'crypto';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { Pool } from 'pg';

const ajv = new Ajv({ removeAdditional: true });
addFormats(ajv);
const schema = {
  type: 'object',
  properties: {
    event: { type: 'string', minLength: 1 },
    occurred_at: { type: 'string', format: 'date-time' },
    url: { type: 'string' },
    referrer: { type: ['string', 'null'] },
    ua: { type: 'string' },
    consent: {
      type: 'object',
      properties: { ad: { type: 'boolean' }, analytics: { type: 'boolean' } },
      required: ['analytics'], additionalProperties: false
    }
  },
  required: ['event', 'occurred_at', 'url', 'ua', 'consent'],
  additionalProperties: true
};
const validate = ajv.compile(schema);

const pool = new Pool({ connectionString: process.env.PG_URL, ssl: { rejectUnauthorized: false } });

export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end();
  try {
    const body = req.body || JSON.parse(req.rawBody || '{}');
    if (!validate(body)) return res.status(400).json({ error: 'invalid_schema', details: validate.errors });

    const ip = (req.headers['x-forwarded-for'] || '').toString().split(',')[0].trim();
    const salt = process.env.HASH_SALT || 'rotate_me';
    const anon_ip = ip ? crypto.createHmac('sha256', salt).update(ip).digest('hex').slice(0, 16) : null;
    const ua = body.ua || (req.headers['user-agent'] || '');
    const server_received_at = new Date().toISOString();

    const text = `insert into events(event_name, occurred_at, url, referrer, anon_ip, ua, consent, props, server_received_at)
                  values($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9)`;
    const values = [
      body.event,
      body.occurred_at,
      body.url,
      body.referrer || null,
      anon_ip,
      ua,
      JSON.stringify(body.consent || {}),
      JSON.stringify(body),
      server_received_at
    ];
    await pool.query(text, values);
    return res.status(204).end();
  } catch (e) {
    console.error(e);
    return res.status(500).json({ error: 'server_error' });
  }
}

AJV(JSON Schemaバリデータ)でのスキーマ検証と、HMACによるIP匿名化を行ったうえでJSONBに原文を格納します。VercelのServerless Functionsにはフリープランがあり、初期段階のコスト最適化に適しています³。

PostgreSQLのDDLとインデックス設計

create table if not exists events (
  id bigserial primary key,
  event_name text not null,
  occurred_at timestamptz not null,
  server_received_at timestamptz not null default now(),
  url text not null,
  referrer text,
  anon_ip text,
  ua text,
  consent jsonb not null,
  props jsonb not null
);
create index if not exists idx_events_name_time on events(event_name, occurred_at desc);
create index if not exists idx_events_gin on events using gin (props jsonb_path_ops);

クエリの大半はイベント種類と期間で絞り込むため、複合インデックスを付与します。原文propsはJSONBの部分一致で拾えるようにGINインデックス(JSON検索向けのインデックス)を追加します。

dbt Coreでマート化(セッション集計の例)

-- models/mart_sessions.sql
with base as (
  select
    anon_ip,
    (date_trunc('minute', occurred_at) - ((extract(epoch from occurred_at)::int % 1800) || ' seconds')::interval) as session_bucket,
    min(occurred_at) as session_start,
    max(occurred_at) as session_end,
    count(*) as events
  from {{ ref('stg_events') }}
  group by 1,2
)
select *,
  extract(epoch from (session_end - session_start)) / 60.0 as duration_min
from base;
# models/schema.yml
version: 2
models:
  - name: mart_sessions
    description: セッション単位の集計
    columns:
      - name: anon_ip
        tests: [not_null]
      - name: session_start
        tests: [not_null]

ステージングモデルでキー正規化や無効イベント除外を済ませ、マートでは意思決定に必要な粒度に落とします。dbt CoreはOSSとして十分に運用可能で、将来Cloudへ移行してもモデル資産は共通です⁴。

セグメント抽出とメール配信(Brevoのフリープランを利用)

// scripts/send_campaign.js
import { Pool } from 'pg';
import fetch from 'node-fetch';

const pool = new Pool({ connectionString: process.env.PG_URL, ssl: { rejectUnauthorized: false } });

async function main() {
  const { rows } = await pool.query(`
    select distinct props - 'ua' - 'consent' as p
    from events
    where event_name = 'signup_completed' and occurred_at > now() - interval '7 days'
  `);
  const recipients = rows
    .map(r => r.p.email)
    .filter(Boolean)
    .map(e => ({ email: e }));
  if (recipients.length === 0) return;
  const res = await fetch('https://api.brevo.com/v3/smtp/email', {
    method: 'POST',
    headers: { 'api-key': process.env.BREVO_API_KEY, 'content-type': 'application/json' },
    body: JSON.stringify({
      sender: { email: 'noreply@yourdomain.com', name: 'Your Product' },
      to: recipients.slice(0, 300),
      subject: 'ようこそ。次のステップをご案内します',
      htmlContent: '<p>ご登録ありがとうございます。…</p>'
    })
  });
  if (!res.ok) throw new Error(await res.text());
}
main().catch(e => { console.error(e); process.exit(1); });

Brevoのフリープランには1日300通の送信枠があります⁷。週次のオンボーディングなど限定的な用途ならコストをかけずに開始できます。配信停止や同意状態はテーブル側で必ずフィルタします。

負荷試験シナリオ(k6)と目安

// k6 run k6.js
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
  vus: 50,
  duration: '1m'
};
export default function () {
  const payload = JSON.stringify({
    event: 'page_view', occurred_at: new Date().toISOString(), url: 'https://example.com', ua: 'k6'
  });
  http.post('https://your-domain.vercel.app/api/events', payload, { headers: { 'Content-Type': 'application/json' } });
  sleep(0.1);
}

VercelのServerlessとNeonの構成で簡易検証を行うと、低遅延で安定動作しやすいことが多い一方、トラフィック量やリージョン、同時接続数に依存します。大量計測が見込まれる場合は、受信をキュー(例:一時的にファイル/R2やメッセージキュー)に積んでバッチ挿入へ寄せるとピークを吸収できます。エッジ側の受け口としてはCloudflare Workersの無償枠活用も有効です²。

運用とガバナンス:スキーマ・テスト・可観測性

イベント設計はJSON SchemaをGitでバージョン管理し、Pull Requestでレビューする体制にします。スキーマに対応するdbtのテストを用意すれば、計測やETLの変更が下流に与える影響を事前に検出できます。CIはGitHub Actionsの無償枠で十分に回せます。ステージング環境ではダミーデータでE2Eを通し、プロダクションではイベント受信のレイテンシ、DBの挿入失敗率、可視化クエリのp95といった指標をダッシュボード化します。OSSのGrafanaやMetabaseでメトリクスの可視化は可能です⁵。ログはVercelやCloudflareの標準ロギングに集約し、失敗イベントはリトライ・DLQに退避して後続の再処理を自動化します。広告やメールといった外部アクティベーションは、同意状態をファーストクラスの列として保持し、運用者が誤って送信対象に含めない仕組みを先にコード化します。SLO(サービス目標値)は、レイヤーごとに定義して運用します。

# .github/workflows/validate.yml
name: validate
on: [pull_request]
jobs:
  schema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm i -g ajv-cli
      - run: ajv validate -s schema/event.json -d samples/**/*.json --errors=text

コストを抑えて始めるからこそ、人手の属人化を避ける自動テストとドキュメントを先に整えます。スキーマの履歴はそのままデータ辞書になり、ツール乗り換えの際も翻訳コストを最小化できます。成果指標は、イベントの完了率、モデリングの日次完了率、可視化の更新遅延、アクティベーションの到達率といった、レイヤーごとのSLOで管理します。

まとめ:ゼロから始め、交換可能に育てる

選択肢は数多くあるものの、足りないのはツールではなく設計でした。イベントスキーマを契約として固定し、サーバーサイド計測でデータ主権を確保し、dbtで意図をコード化し、無償枠で回る範囲から始める。この順序なら、現場の計測・分析・施策は今日からでも動かせます。まずは重要な3つのイベントに絞り、上のサンプルをそのままデプロイして、翌日のダッシュボード更新が滞りなく終わるかを確かめてください。うまく回り始めたら、セグメントの精度やアクティベーションの自動化を段階的に増やしていけば良いのです。コストを抑えつつも、意思決定の速度は確実に上げられます。あなたの組織では、どのレイヤーから始めますか。

参考文献

  1. Chiefmartec. 2023 Marketing Technology Landscape Supergraphic: 11,038 solutions.
  2. Cloudflare Developers. Workers pricing (Free plan limits).
  3. Vercel Docs. Serverless Functions Pricing.
  4. dbt Labs. Licensing dbt (dbt Core is open source).
  5. Metabase. Open source analytics.
  6. Airbyte Blog. The modern open data stack: four core tools.
  7. Brevo Help Center. FAQs: Are there any sending limits (emails and SMS)?
  8. HubSpot. Free CRM software.
  9. Osano/cookieconsent. GitHub repository.
  10. Miralytics. Server-side tracking overview.
  11. arXiv. 1912.05861v1.