Γιατί MySQL και όχι GA4 ή BigQuery;

Το GA4 είναι εξαιρετικό για analytics, αλλά έχει ένα δομικό μειονέκτημα που σε peak περιόδους γίνεται επικίνδυνο: δεν είναι real-time revenue system. Σε events υψηλού όγκου (π.χ. Black Friday) το reporting lag οδηγεί σε λάθος αποφάσεις. Όταν το ad spend πρέπει να αλλάξει άμεσα, ένα ανεξάρτητο logging layer από nice-to-have γίνεται must.

Το Web GTM → Server-Side GTM → MySQL pipeline:

  • πιάνει κάθε purchase τη στιγμή που συμβαίνει
  • το αποθηκεύει σε δικό σου server / database
  • λειτουργεί ανεξάρτητα από analytics/ad platforms
  • είναι ιδανικό για real-time dashboards και άμεσες budget αποφάσεις

Με MySQL για order logging κερδίζεις:

  • Real revenue σε milliseconds από τη στιγμή της αγοράς
  • Μηδενική εξάρτηση από GA4 latency / processing / sampling
  • Δικά σου dashboards σε Power BI, Looker Studio ή Metabase
  • Εύκολη διασύνδεση με custom εργαλεία, scripts, alerts
  • Reconciliation με ERP ή site orders για να βρίσκεις αποκλίσεις
  • Free σε επίπεδο software — open-source stack + δική σου υποδομή

Αποτέλεσμα: marketing και performance ομάδα βλέπουν revenue όπως πραγματικά τρέχει, χωρίς καθυστερήσεις και χωρίς να “περιμένουν” τρίτους.

Architecture overview

Το σύστημα το στήνουμε σε τρία καθαρά layers, με ξεκάθαρους ρόλους:

  1. Browser / dataLayer — κάνει emit το purchase event με όλα τα order details
  2. GTM Web Container — διαβάζει το dataLayer και προωθεί τα δεδομένα στο Server-Side GTM
  3. Server-Side GTM → MySQL API — κάνει processing/enrichment όπου χρειάζεται και γράφει το purchase στη βάση

Data flow:

Browser (dataLayer.purchase)
   ↓
GTM Web (Tag: Send to Server)
   ↓
Server-Side GTM (Client + Tag)
   ↓
REST API Endpoint (Flask / Node / PHP)
   ↓
MySQL (orders table)

Κάθε layer κάνει μόνο αυτό που πρέπει. Όταν η αρχιτεκτονική είναι καθαρή, το maintenance, το debugging και η επέκταση (π.χ. alerts, deduplication, joins με ERP) γίνονται σημαντικά πιο εύκολα.

Το dataLayer είναι η βάση όλων

Πριν πιάσεις GTM (web ή server-side), το e-shop πρέπει να κάνει emit ένα σωστά δομημένο purchase event στο dataLayer. Αν το event είναι ελλιπές ή “πειραγμένο”, όλο το pipeline πατάει σε μισές αλήθειες — και αυτό καταλήγει σε λάθος revenue νούμερα και bugs που δεν εντοπίζονται εύκολα.

Recommended purchase event structure (GA4 Enhanced E-commerce format):

dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "ORDER12345",
    value: 74.90,
    currency: "EUR",
    tax: 0,
    shipping: 3.90,
    discount: 5.00,
    coupon: "BF2025",
    items: [
      {
        item_id: "SKU-00123",
        item_name: "Oversized Hoodie",
        item_brand: "BrandX",
        item_category: "Apparel > Women > Hoodies",
        item_variant: "Black / Medium",
        price: 39.95,
        quantity: 1
      }
    ]
  }
});

Συνηθισμένα dataLayer προβλήματα που πρέπει να λυθούν πρώτα “upstream” (στο e-shop):

  • λείπει order_id
  • λείπει coupon
  • δεν υπάρχει διαχωρισμός σε subtotal, discount, tax
  • λείπουν item-level fields (brand, category, variant)
  • το event πυροδοτείται λάθος στιγμή (refresh, back navigation, SPA transitions)

Διόρθωσέ τα στην πηγή. Το dataLayer είναι το “data contract” που τροφοδοτεί όλα τα downstream layers.

Web GTM → Server-Side GTM

Στο Web GTM αποφεύγουμε επίτηδες να στείλουμε όλο το dataLayer προς Server-Side GTM — αυξάνει processing/transfer κόστος χωρίς ουσιαστικό όφελος. Φτιάχνουμε Data Layer Variables μόνο για τα πεδία που χρειαζόμαστε (transaction_id, value, currency, shipping, tax, discount, items) και στέλνουμε μόνο αυτά μέσω Stape Data Tag.

Το tag κάνει trigger αποκλειστικά στο purchase event, ώστε κάθε order να στέλνεται μία φορά προς Server-Side GTM — χωρίς duplication και χωρίς άσκοπα calls.

Στο server side, ο Data Client παραλαμβάνει τις incoming variables και τις μετατρέπει σε event μέσα στο Server-Side GTM container. Σε Debug Mode βλέπεις ένα νέο event ανά purchase, με ακριβώς τα fields που προώθησες — transaction_id, value, currency, shipping, tax, discount και όλο το items list. Δεν χρειάζονται transforms· οι τιμές έρχονται όπως είναι.

Server-Side GTM → Flask endpoint

Το Server-Side GTM προωθεί το purchase στο δικό μας backend μέσω JSON HTTP Request tag που δείχνει σε κάτι σαν https://api.mydomain.gr/gtm/purchase-log. Το payload κουβαλά μόνο ό,τι χρειαζόμαστε — transaction_id, value, currency, shipping, tax, discount, items list — και λίγο βασικό context (timestamp, domain).

Minimal Flask app που παραλαμβάνει και γράφει σε MySQL:

from flask import Flask, request, jsonify
import mysql.connector
from datetime import datetime
import json

app = Flask(__name__)

db_config = {
  "host": "localhost",
  "user": "my_user",
  "password": "my_password",
  "database": "my_database",
  "charset": "utf8mb4"
}

@app.route("/gtm/purchase-log", methods=["POST"])
def purchase_log():
  data = request.get_json(silent=True) or {}
  order_id = data.get("transaction_id")
  value    = data.get("value")

  if not order_id or value is None:
    return jsonify({"status": "error", "message": "Missing order_id or value"}), 400

  conn = mysql.connector.connect(**db_config)
  cursor = conn.cursor()
  cursor.execute("""
    INSERT INTO orders_log
    (order_id, order_value, currency, shipping, tax, discount, items_json, created_at, source)
    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
  """, (
    order_id, value,
    data.get("currency", "EUR"),
    data.get("shipping", 0), data.get("tax", 0), data.get("discount", 0),
    json.dumps(data.get("items", []), ensure_ascii=False),
    datetime.utcnow(),
    "gtm-ss"
  ))
  conn.commit()
  cursor.close()
  conn.close()
  return jsonify({"status": "ok"}), 200

MySQL table schema:

orders_log
----------
id             AUTO_INCREMENT PRIMARY KEY
order_id       VARCHAR
order_value    DECIMAL
currency       VARCHAR
shipping       DECIMAL
tax            DECIMAL
discount       DECIMAL
items_json     JSON or TEXT
created_at     DATETIME
source         VARCHAR  -- e.g. "gtm-ss"

Για πρώτη υλοποίηση, το να αποθηκεύσεις τα items ως JSON είναι μια χαρά. Normalization σε order_items table μπορεί να μπει μετά, όταν το χρειαστείς για πιο βαριά BI queries.

Apache reverse proxy + HTTPS

Το Server-Side GTM χρειάζεται public HTTPS URL για να κάνει call, αλλά το Flask συνήθως τρέχει σε 127.0.0.1:5000. Ένα Apache reverse proxy αναλαμβάνει SSL termination σε public host και προωθεί το request εσωτερικά προς το Flask.

Ενεργοποιείς τα απαραίτητα modules και στήνεις Virtual Host στο 443 με Let's Encrypt certificates, ProxyPass προς το Flask instance, και Header always set directives για CORS αν το Server-Side GTM domain διαφέρει από το API domain. Με αυτά στη θέση τους, το Server-Side GTM μπορεί να κάνει POST purchases στο https://api.mysite.gr/gtm/purchase-log — και κάθε order γράφεται στη MySQL μέσα σε milliseconds από το click στο "confirm".