GTM Server-Side Tag

A custom tag template for a server-side Google Tag Manager container. It forwards every GA4-model event the container receives to Drengr, reshaped into a Segment-spec call — no client-side changes at all.

Test before production use. This template is hand-verified against the documented sandboxed-JS API surface (sendHttpRequest, getAllEventData, JSON, logToConsole) and the template's permission/parameter schema — it has not yet been run against a live server GTM container. Validate it in GTM's Preview mode against your own container before publishing to production traffic.

5-minute install

  1. Copy the template source below into a new file named drengr.tpl.
  2. In your server GTM container: Templates → Tag Templates → New → Import, and select that file.
  3. Tags → New, pick the Drengr tag type that now appears.
  4. Paste your Drengr write key (drengr_pk_…, from /pro) into the Drengr Write Key field. Leave Endpoint override as-is unless told otherwise.
  5. Trigger: All Events (or scope it to the specific GA4 events you already route through this container).
  6. Publish.

Template source

The complete .tplfile GTM's import expects — copy everything below verbatim into drengr.tpl:

tpl
___TERMS_OF_SERVICE___

By creating or modifying this file you agree to Google Tag Manager's Community
Template Gallery Developer Terms of Service available at
https://developers.google.com/tag-manager/gallery-tos (or such other URL as
Google may provide), as modified from time to time.


___INFO___

{
  "type": "TAG",
  "id": "cvt_temp_public_id",
  "version": 1,
  "securityGroups": [],
  "displayName": "Drengr",
  "brand": {
    "id": "drengr",
    "displayName": "Drengr"
  },
  "description": "Forwards every GA4-model event this server container receives to Drengr, so it lands as a named event in your Drengr analysis layer. No client-side changes required.",
  "containerContexts": [
    "SERVER"
  ]
}


___TEMPLATE_PARAMETERS___

[
  {
    "type": "TEXT",
    "name": "writeKey",
    "displayName": "Drengr Write Key",
    "simpleValueType": true,
    "valueValidators": [
      {
        "type": "NON_EMPTY"
      }
    ],
    "help": "Your Drengr publishable write key (starts with drengr_pk_). Find it in the Drengr console under Settings → API Keys."
  },
  {
    "type": "TEXT",
    "name": "endpointUrl",
    "displayName": "Endpoint override (advanced)",
    "simpleValueType": true,
    "defaultValue": "https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/segment-ingest",
    "help": "Leave as-is unless Drengr support told you to point at a different ingest URL."
  }
]


___SANDBOXED_JS_FOR_SERVER___

/**
 * Drengr — GA4 event forwarder (server-side GTM tag).
 * Reads the GA4-model event this container just processed, reshapes it into
 * a Segment-spec track call, and POSTs it to Drengr's segment-ingest
 * endpoint. Drengr's endpoint does its own event-name normalization, so the
 * raw event_name is forwarded verbatim.
 *
 * PII: GA4's user_data (email/phone/address, hashed or raw) is dropped
 * entirely before anything leaves this container — never forwarded.
 */

const getAllEventData = require('getAllEventData');
const sendHttpRequest = require('sendHttpRequest');
const JSON = require('JSON');
const logToConsole = require('logToConsole');

const eventData = getAllEventData();

const anonymousId = eventData.client_id || '';
const userId = eventData.user_id || '';
const eventName = eventData.event_name || '';
const ts = eventData.timestamp; // rarely set on GA4 hits; forwarded only if present

// context.app/os/device — best-effort. GA4's documented "common event data"
// has no app/os/device fields for web hits; these only show up on
// Firebase/GA4(App)-origin events. Forwarded when present, never fabricated.
const context = {};
if (eventData.app_id || eventData.app_version) {
  context.app = {};
  if (eventData.app_id) context.app.name = eventData.app_id;
  if (eventData.app_version) context.app.version = eventData.app_version;
}
if (eventData.os_name || eventData.platform || eventData.os_version) {
  context.os = {
    name: eventData.os_name || eventData.platform || ''
  };
  if (eventData.os_version) context.os.version = eventData.os_version;
}
if (eventData.device_model || eventData.device_brand) {
  context.device = { model: eventData.device_model || eventData.device_brand };
}
const hasContext = !!(context.app || context.os || context.device);

// properties — every remaining event-data key, minus internals:
//  - x-ga-*   : GA4 client-internal plumbing, never meant to leave the container
//  - user_data: GA4's PII slot (hashed/raw email, phone, address) — never forwarded
//  - the fields already placed above (event_name/client_id/user_id/timestamp)
const properties = {};
for (var key in eventData) {
  if (key === 'event_name' || key === 'client_id' || key === 'user_id' || key === 'timestamp') continue;
  if (key === 'user_data') continue;
  if (key.indexOf('x-ga-') === 0) continue;
  properties[key] = eventData[key];
}

const body = {
  writeKey: data.writeKey,
  type: 'track',
  event: eventName,
  anonymousId: anonymousId,
  properties: properties
};
if (userId) body.userId = userId;
if (ts) body.timestamp = ts;
if (hasContext) body.context = context;

const endpoint = data.endpointUrl || 'https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/segment-ingest';

sendHttpRequest(endpoint, {
  headers: { 'Content-Type': 'application/json' },
  method: 'POST',
  timeout: 5000
}, JSON.stringify(body)).then((result) => {
  if (result.statusCode >= 200 && result.statusCode < 300) {
    data.gtmOnSuccess();
  } else {
    logToConsole('Drengr tag: non-2xx from segment-ingest', result.statusCode, result.body);
    data.gtmOnFailure();
  }
}, (error) => {
  logToConsole('Drengr tag: request failed', error);
  data.gtmOnFailure();
});


___SERVER_PERMISSIONS___

[
  {
    "instance": {
      "key": {
        "publicId": "read_event_data",
        "versionId": "1"
      },
      "param": [
        {
          "key": "eventDataAccess",
          "value": {
            "type": 1,
            "string": "any"
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  },
  {
    "instance": {
      "key": {
        "publicId": "send_http",
        "versionId": "1"
      },
      "param": [
        {
          "key": "allowedUrls",
          "value": {
            "type": 1,
            "string": "specific"
          }
        },
        {
          "key": "urls",
          "value": {
            "type": 2,
            "listItem": [
              {
                "type": 1,
                "string": "https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/segment-ingest*"
              },
              {
                "type": 1,
                "string": "https://*.supabase.co/*"
              }
            ]
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  },
  {
    "instance": {
      "key": {
        "publicId": "logging",
        "versionId": "1"
      },
      "param": [
        {
          "key": "environments",
          "value": {
            "type": 1,
            "string": "debug"
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  }
]


___NOTES___

Built for Drengr's segment-ingest endpoint (Segment HTTP API-compatible track
calls). Source: integrations/gtm-server-tag/ in the Drengr repo.

What gets forwarded

Every event is reshaped into a Segment-spec track call and POSTed to the segment-ingest endpoint — its event mapping (dims/measures/install_id/external_id) applies from there.

Source fieldsemantic_events fieldNotes
event_nameevent (track)Forwarded verbatim — Drengr normalizes on receipt.
client_idanonymousId
user_iduserIdOmitted from the payload entirely if not set.
timestamp (if the event carries one)timestampRarely set on web GA4 hits — Drengr stamps receive-time otherwise.
app_id / app_versioncontext.app.name / context.app.versionFirebase/GA4-App-origin events only — web hits don't carry these.
os_name or platform / os_versioncontext.os.name / context.os.version
device_model or device_brandcontext.device.model
everything else (page_location, page_title, value, currency, custom params, …)properties.*
user_data (hashed/raw email, phone, address)(dropped, never forwarded)GA4's PII slot — stripped before the request body is built, full stop.
x-ga-* keys(dropped)GA4 client-internal plumbing.

Permissions this tag requests

  • read_event_data (any) — needed because it forwards arbitrary/unknown event params, not a fixed field list.
  • send_http, scoped to Drengr's ingest endpoint and *.supabase.co — no other destination is reachable from this tag.
  • logging, scoped to debug/preview only — failures are logged to the GTM preview console, nothing is logged in production.

Under the hood

The sandboxed JS (full source above) reads the event via getAllEventData(), builds the Segment track body shown in the mapping table above, and POSTs it with sendHttpRequest — a 2xx response calls data.gtmOnSuccess(), anything else calls data.gtmOnFailure() and logs the status/body to the GTM debug console via logToConsole.

Troubleshooting

Confirm you're importing into a server container — this template declares containerContexts: ["SERVER"]and won't show up as an option in a web container.