___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. Use this section to manually set User Identifiers.\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cstrong\u003eImportant:\u003c/strong\u003e Do not hash data yourself, as this Tag handles both data formatting and hashing for you. Already hashed data will be discarded. You can check the hashed result in GTM Preview mode." }, { "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.\u003cbr/\u003e\n👉 \u003cstrong\u003eInherit\u003c/strong\u003e: Will use Google Consent Mode, if available.", "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.\u003cbr/\u003e\n👉 \u003cstrong\u003eInherit\u003c/strong\u003e: Will use Google Consent Mode, if available.", "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\u003cbr/\u003e\n👉 \u003cstrong\u003eInherit\u003c/strong\u003e: Will use Google Consent Mode, if available.", "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\u003cbr/\u003e\n👉 \u003cstrong\u003eInherit\u003c/strong\u003e: Will use Google Consent Mode, if available.", "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-z0-9!#$%&\'*+/=?^\\`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_\\`{|}~-]+)*|"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])', 'i'); 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++) { handleTableAlias(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, 'STANDARD'); user.sha256_email_address_canonical = hashEmail(user.email_address, 'CANONICAL'); 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, format) { if (getType(email) !== 'string' || !testRegex(EMAIL_REGEX, email)) return undefined; const matchedEmail = email.match(EMAIL_REGEX).toString(); const standardEmail = matchedEmail.trim().toLowerCase(); const standardEmailComps = standardEmail.split('@'); const canonicalEmail = standardEmailComps[0] .replace(createRegex('\\+.+', 'gi'), '') // Stripping subaddress .replace(createRegex('[^a-z0-9]', 'gi'), '') // Stripping non-alphanumeric .concat('@') .concat(standardEmailComps[1]); return sha256Sync(format === 'CANONICAL' ? canonicalEmail : standardEmail, { 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 handleTableAlias(alias, result) { var output; switch (alias.key) { case 'email': output = hashEmail(alias.value, 'STANDARD'); if (output !== undefined) { result.push({ alias_type: 'email', hashed_email: output, hashed_email_canonical: hashEmail(alias.value, 'CANONICAL'), }); } 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