___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_MQ72P", "version": 1, "displayName": "OneView Server Tag", "categories": [ "ATTRIBUTION", "CONVERSIONS", "DATA_WAREHOUSING" ], "brand": { "displayName": "oneviewhub", "thumbnail": "data:image/png;base64,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\u003d\u003d", "id": "github.com_oneviewhub" }, "description": "Send events to OneView as a Server-side Tag Manager® Backend Source", "containerContexts": [ "SERVER" ], "securityGroups": [] } ___TEMPLATE_PARAMETERS___ [ { "type": "TEXT", "name": "apiKey", "displayName": "API Key", "simpleValueType": true, "help": "You can find your API Key in your dashboard under Settings", "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "type": "TEXT", "name": "idempotencyKey", "displayName": "(optional) Idempotency Key", "simpleValueType": true, "help": "Multiple events with the same \u003cstrong\u003eIdempotency Key\u003c/strong\u003e are considered duplicates, and only the first occurrence will be considered." }, { "type": "GROUP", "name": "identifiers", "displayName": "User Identifiers", "groupStyle": "NO_ZIPPY", "subParams": [ { "type": "SIMPLE_TABLE", "name": "data", "simpleTableColumns": [ { "defaultValue": "user_id", "displayName": "Type", "name": "key", "type": "SELECT", "selectItems": [ { "value": "client", "displayValue": "💻 Client ID" }, { "value": "user", "displayValue": "👤 User ID" }, { "value": "organization", "displayValue": "🏢 Organization ID" }, { "value": "email", "displayValue": "📧 Email Address" }, { "value": "phone", "displayValue": "📞 Phone Number (E.164)" } ], "macrosInSelect": false, "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "defaultValue": "", "displayName": "Plaintext Value (non-hashed)", "name": "value", "type": "TEXT", "valueValidators": [ { "type": "NON_EMPTY" } ], "isUnique": true } ], "newRowButtonText": "Add New", "valueValidators": [], "alwaysInSummary": false } ], "help": "All identifiers in this container\u0027s \u003ca href\u003d\"https://developers.google.com/tag-platform/tag-manager/server-side/common-event-data\"\u003eCommon Event Data\u003c/a\u003e are automatically included." }, { "type": "GROUP", "name": "consent", "displayName": "Consent Settings", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "SELECT", "name": "analyticsStorage", "displayName": "analytics_storage", "macrosInSelect": true, "selectItems": [ { "value": true, "displayValue": "✅ Granted (true)" }, { "value": false, "displayValue": "❌ Denied (false)" } ], "simpleValueType": true, "help": "✅ \u003cstrong\u003eGranted\u003c/strong\u003e: Event is processed by OneView.\u003cbr/\u003e\n❌ \u003cstrong\u003eDenied\u003c/strong\u003e: Event will be discarded by OneView.", "notSetText": "Inherit" }, { "type": "SELECT", "name": "adStorage", "displayName": "ad_storage", "macrosInSelect": true, "selectItems": [ { "value": true, "displayValue": "✅ Granted (true)" }, { "value": false, "displayValue": "❌ Denied (false)" } ], "simpleValueType": true, "help": "If this event is a Conversion Event:\u003cbr/\u003e\u003cbr/\u003e\n✅ \u003cstrong\u003eGranted\u003c/strong\u003e: Conversion API will send data to your Media Partners.\u003cbr/\u003e\n❌ \u003cstrong\u003eDenied\u003c/strong\u003e: Conversion API will not fire, and your Media Partners will not receive data about this conversion.", "notSetText": "Inherit" }, { "type": "SELECT", "name": "adUserData", "displayName": "ad_user_data", "macrosInSelect": true, "selectItems": [ { "value": true, "displayValue": "✅ Granted (true)" }, { "value": false, "displayValue": "❌ Denied (false)" } ], "simpleValueType": true, "help": "If this event is a Conversion Event, and Conversion API supports enrichment with PII (\"\u003ci\u003eEnhanced Conversions\u003c/i\u003e\"):\u003cbr/\u003e\u003cbr/\u003e\n✅ \u003cstrong\u003eGranted\u003c/strong\u003e: PII are added to Conversion API\u003cbr/\u003e\n❌ \u003cstrong\u003eDenied\u003c/strong\u003e: PII are not included to Conversion API", "notSetText": "Inherit" }, { "type": "SELECT", "name": "adPersonalization", "displayName": "ad_personalization", "macrosInSelect": true, "selectItems": [ { "value": true, "displayValue": "✅ Granted (true)" }, { "value": false, "displayValue": "❌ Denied (false)" } ], "simpleValueType": true, "help": "If this event is a Conversion Event, and Conversion API supports flag for personalized advertising:\u003cbr/\u003e\u003cbr/\u003e\n✅ \u003cstrong\u003eGranted\u003c/strong\u003e: Flag set to true in Conversion API\u003cbr/\u003e\n❌ \u003cstrong\u003eDenied\u003c/strong\u003e: Flag set to false in Conversion API", "notSetText": "Inherit" } ] } ] ___SANDBOXED_JS_FOR_SERVER___ const sendHttpRequest = require('sendHttpRequest'); const getAllEventData = require('getAllEventData'); const getEventData = require('getEventData'); const makeString = require('makeString'); const getType = require('getType'); const JSON = require('JSON'); const logToConsole = require('logToConsole'); const getContainerVersion = require('getContainerVersion'); const sha256Sync = require('sha256Sync'); const createRegex = require('createRegex'); const testRegex = require('testRegex'); const containerVersion = getContainerVersion(); const isLoggingEnabled = containerVersion.debugMode; const EMAIL_REGEX = createRegex('[a-zA-Z0-9_.+\\-]+[\\x40][a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}', ''); const PHONE_REGEX = createRegex('^\\+[1-9]\\d{1,14}$', ''); const SHA256_REGEX = createRegex('^[a-fA-F0-9]{64}$', ''); const ENDPOINT = 'https://earth.oneviewhub.cloud/v1/sgtm'; const TIMEOUT_MS = 1500; const OMIT_PLAINTEX_PII = true; /* ------------------------------------------------ Found a bug or want to contribute? Submit issues or pull requests at: https://github.com/oneviewhub/gtm-server-tag -------------------------------------------------- */ // ---------------------------- // Request Payload // ---------------------------- var fallbackConsentMode = extractConsentMode(); var postBodyData = { event_name: getEventData('event_name') || 'unknown', event_payload: getAllEventData(), event_metadata: { consent: { ad_user_data: handleConsent(data.adUserData, fallbackConsentMode.ad_user_data), ad_personalization: handleConsent(data.adPersonalization, fallbackConsentMode.ad_personalization), analytics_storage: handleConsent(data.analyticsStorage, fallbackConsentMode.analytics_storage), ad_storage: handleConsent(data.adStorage, fallbackConsentMode.ad_storage), }, identifiers: (tableData => { if (!tableData) return []; var result = [], i; for (i = 0; i < tableData.length; i++) { handleAlias(tableData[i], result); } return result; })(data.data), } }; // ---------------------------- // PII Hashing & Removal // ---------------------------- var user = postBodyData.event_payload.user_data; if (user) { user.sha256_email_address = hashEmail(user.email_address); user.sha256_phone_number = hashPhone(user.phone_number, 'E.164'); user.sha256_phone_number_digits = hashPhone(user.phone_number, 'NUMERIC'); if (OMIT_PLAINTEX_PII) { user.email_address = undefined; user.phone_number = undefined; } var addr = user.address; if (addr) { addr.first_name = hashName(addr.first_name); addr.last_name = hashName(addr.last_name); if (OMIT_PLAINTEX_PII) { addr.first_name = undefined; addr.last_name = undefined; } } } // ---------------------------- // Request // ---------------------------- if (isLoggingEnabled) { logToConsole( JSON.stringify({ Name: 'OneViewEvent', Type: 'Request', RequestMethod: 'POST', RequestUrl: ENDPOINT, RequestBody: postBodyData, }) ); } sendHttpRequest( ENDPOINT, (statusCode, headers, body) => { if (isLoggingEnabled) { logToConsole( JSON.stringify({ Name: 'OneViewEvent', Type: 'Response', ResponseStatusCode: statusCode, ResponseHeaders: headers, ResponseBody: body, }) ); } if (statusCode >= 200 && statusCode < 300) { data.gtmOnSuccess(); } else { data.gtmOnFailure(); } }, { headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + makeString(data.apiKey), 'Idempotency-Key': (() => { if (!data.idempotencyKey) return undefined; var output = makeString(data.idempotencyKey).trim(); if (output !== '') { return output; } else { return undefined; } })(getType(data.idempotencyKey)), }, method: 'POST', timeout: TIMEOUT_MS, }, JSON.stringify([postBodyData]) ); // ---------------------------- // Hashing Helpers // ---------------------------- function hashEmail(email) { if (getType(email) !== 'string' || !testRegex(EMAIL_REGEX, email)) return undefined; return sha256Sync(email.trim().toLowerCase(), { outputEncoding: 'hex' }); } function hashPhone(phone, format) { if (getType(phone) !== 'string' || !testRegex(PHONE_REGEX, phone)) return undefined; return sha256Sync(format === 'NUMERIC' ? phone.slice(1) : phone, { outputEncoding: 'hex' }); } function hashName(name) { if (getType(name) !== 'string' || testRegex(SHA256_REGEX, name) || name.trim() === '') return undefined; return sha256Sync(name.trim().toLowerCase(), { outputEncoding: 'hex' }); } function noHash(value) { if (getType(value) !== 'string' || testRegex(SHA256_REGEX, value) || value.trim() === '') return undefined; return value; } function handleAlias(alias, result) { var output; switch (alias.key) { case 'email': output = hashEmail(alias.value); if (output !== undefined) { result.push({ alias_type: 'email', hashed_email: output, }); } break; case 'phone': output = hashPhone(alias.value, 'E.164'); if (output !== undefined) { result.push({ alias_type: 'phone', hashed_e164: output, hashed_e164_numeric: hashPhone(alias.value, 'NUMERIC'), }); } break; case 'user': output = noHash(alias.value); if (output !== undefined) { result.push({ alias_type: 'user', user_id: alias.value, }); } break; case 'organization': output = noHash(alias.value); if (output !== undefined) { result.push({ alias_type: 'organization', organization_id: alias.value, }); } break; case 'client': output = noHash(alias.value); if (output !== undefined) { result.push({ alias_type: 'client', anonymous_client_id: alias.value, }); } break; default: break; } } function handleConsent(explicitConsent, fallbackConsent) { var type = getType(explicitConsent); if (type === 'boolean') return explicitConsent; if (type === 'number') { if (explicitConsent === 1) return true; if (explicitConsent === 0) return false; } if (type === 'string') { var consentString = explicitConsent.trim().toLowerCase(); if (consentString === 'undefined' || consentString === '' || consentString === 'inherit' || consentString === 'null') { return fallbackConsent; } if (consentString === 'true' || consentString === 'granted') { return true; } if (consentString === 'false' || consentString === 'denied') { return false; } } return fallbackConsent; } // ---------------------------------- // Consent Helpers // ---------------------------------- function extractConsentMode() { // ---- Consent Mode v2 (GCD) ---- const gcdLocation1 = getEventData('x-sst-system_properties.gcd'); const gcdLocation2 = getEventData('x-ga-gcd'); function parseGcdConsentString(consentSignals) { if (consentSignals.length < 8) { return { ad_storage: null, analytics_storage: null, ad_user_data: null, ad_personalization: null, }; } // Function to determine consent status based on the provided value function determineConsentStatus(value) { switch (value) { case 't': return true; // granted by default (no update) case 'r': return true; // denied by default and granted after update. case 'n': return true; // granted after update (no default). case 'v': return true; // granted both by default and after update. case 'p': return false; // denied by default (no update). case 'q': return false; // denied both by default and after update. case 'm': return false; // denied after update (no default) case 'u': return false; // granted by default and denied after update. case 'l': return null; // The lowercase L means that the signal has not been set with Consent Mode. default: return null; } } // Return an object containing consent values return { ad_storage: determineConsentStatus(consentSignals[2]), analytics_storage: determineConsentStatus(consentSignals[4]), ad_user_data: determineConsentStatus(consentSignals[6]), ad_personalization: determineConsentStatus(consentSignals[8]), }; } const gcd = (() => { if (getType(gcdLocation1) === 'string' && gcdLocation1 !== '') { return gcdLocation1; } else if (getType(gcdLocation2) === 'string' && gcdLocation2 !== '') { return gcdLocation2; } else { return undefined; } })(); // ---- Consent Mode v1 (GCS) ---- function parseGcsConsentString(consentString) { if (consentString.length !== 4) { return { ad_storage: null, analytics_storage: null, ad_user_data: null, ad_personalization: null, }; } // Extract the values for ad_storage and analytics_storage var adStorageValue = consentString.substring(2, 3); var analyticsStorageValue = consentString.substring(3, 4); // Function to determine consent status based on the provided value function determineConsentStatus(value) { switch (value) { case '1': return true; // Granted case '0': return false; // Denied default: return null; // Invalid value or not set, return undefined } } // Determine consent status for ad_storage and analytics_storage var adStorageConsent = determineConsentStatus(adStorageValue); var analyticsStorageConsent = determineConsentStatus(analyticsStorageValue); // Return an object containing consent values return { ad_storage: adStorageConsent, analytics_storage: analyticsStorageConsent, ad_user_data: null, ad_personalization: null, }; } const gcsLocation1 = getEventData('x-ga-gcs'); const gcs = (() => { if (getType(gcsLocation1) === 'string' && gcsLocation1 !== '') { return gcsLocation1; } else { return undefined; } })(); const output = (() => { if (getType(gcd) === 'string') { return parseGcdConsentString(gcd); } else if (getType(gcs) === 'string') { return parseGcsConsentString(gcs); } else { return { ad_storage: null, analytics_storage: null, ad_user_data: null, ad_personalization: null, }; } })(); return output; } ___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://*.oneviewhub.cloud/v1/sgtm" } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "logging", "versionId": "1" }, "param": [ { "key": "environments", "value": { "type": 1, "string": "debug" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "read_container_data", "versionId": "1" }, "param": [] }, "isRequired": true } ] ___TESTS___ scenarios: - name: Check semantical errors code: |- const mockData = { url: "https://httpbin.org/anything", custom: { header: {}, data: {} } }; // Call runCode to run the template's code. runCode(mockData); ___NOTES___ Created on 06/07/2025, 13:50:44