___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": "Creatable Conversion Tracking",
"categories": ["AFFILIATE_MARKETING", "CONVERSIONS", "ATTRIBUTION"],
"brand": {
"id": "brand_creatable",
"displayName": "Creatable",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAPpSURBVHgBxVe9TxRREJ+3xwHK12kIEEFzJAJZEgQKtVIgERspMND4kRDiHwAWFkqIZ9BY2EBjYkHURGNF1IgNFGKsjAWoCQQ0sCoQJSoHB/Kx3D5n9o5z7+1bbg9I/CVX3OzbN/NmZ36/eQxcYsof8OngbfQoUGNwqGIAfjT7oo+DHEBjDEbAYEMeWH9erAWCbvZliR3f9uvKRrvClRaLQzc7P/AY+g0MRNt6maPjgM9QUq9zztthB+CMdZdMXrsMyQRApw4z4xVE0rwb0Dxcr5NlwxbAhP9WlcLgqZPz9OOHIKO+DLxF2aBkp8fsa6NzsDw4Dqtvv4JTEFg7Z0u1jhHHALY6+b62E+BrPRrnVIaN6QX43fMGQn0fpEGImWD/nAd8YeYdFp2nFOZAwb1mSCvPh2RAgcycewQbMwu2IEJcr66OdomyaaWCE52nqflw8OWlpJ2bgRflRN5V88RH/syILxNmBqKpn4rboDCyQaKUJ4KxuArfzvTaMoGfopg+RQr90Vk4oAj1WPjkoqNz2nSx7yOsj343/6eWF0BmfQme2k4TtAd9wumG3ji7Dh5q73YW/fbz1odZTUcg706D1PE8Fljw/juQIaupAnI766WBU2HSuxYEsRaKFaJXcfF+rHiZ89nzjx2dE0KYFVpDa0VQB4mmLFAaMfO81mqlPqcCEvGzaxB7/QckAq0RTmqCsrIH97aCKZ6aFIyg0mrMOF1me3ljOmiezi0oS7JgdaEQSdSoCP1Wo6RtYGngEySLFWdGjIEUlXggocKtjyVO/TbhU+A/gwKIGxyMxTXbolQ1eSZ0iSDVgIa/qk2LbuduyG6ugF83ByEZpAsVTyB9sDIiTVEpBufvFcZiAfwZGLf1LLVQDtoWtuAAK5yIbFYQJxrh8BOwIesiql4ZkRA5yTpEBAlYbucpm51a2dYZOD8qXtCfgVAHMrajLBxAfaDTOSGn9RiuueBIxSJCOLyaCvTZ39WNtNRmfVjU7yzD9C2XBiawPaNipBaYdeIkXnT6LyfvxhtxaD082dFqBjCGcuwV5Zj0vH+X5BiVkIK2QkchUlGOTR5QtasacKMHhFM6CUsyzmkP0bmBvtToWBYjIg+EAxBpyRiIzyPRu7pjgOxdiSZopVpnbNSPBUA3GZ0rdWIQFD19v7krL1wFsjkzTEvSTntj6uushqTHcpLUvaiY1pY0QmugozPikJWdjOWbiBblrl5M6OSq5GIiFSMqShwaq8XC3A6o4GgMVx3uiAkvp5QNKlCk6xZwjyA6fhiGcLe63cupiGEcXjPM+ZHXYtoqkcj9YLmeA+caAzYSBni9jOxa7fJ6/hcf29hS1PWjBAAAAABJRU5ErkJggg=="
},
"description": "Fires Creatable conversion tracking on a purchase, including coupon codes and the new-customer flag. Auto-reads the GA4 ecommerce dataLayer; manual mapping available for non-standard setups.",
"containerContexts": ["WEB"]
}
___TEMPLATE_PARAMETERS___
[
{
"type": "TEXT",
"name": "accountId",
"displayName": "Creatable Account ID",
"simpleValueType": true,
"valueValidators": [
{
"type": "NON_EMPTY"
}
],
"help": "Your Creatable account ID (the li value). Provided by your Creatable account manager. Stage and Production use different account IDs."
},
{
"type": "TEXT",
"name": "cookieDomain",
"displayName": "Cookie domain",
"simpleValueType": true,
"help": "Optional. Scope the Creatable cookie to this domain instead of the site's top-level domain. It must be the page's hostname or a parent of it, for example us.example.com; a container shared by several regional sites should use the {{Page Hostname}} variable. Any other value is ignored and the top-level domain is used. Leave blank to auto-detect."
},
{
"type": "RADIO",
"name": "environment",
"displayName": "Environment",
"radioItems": [
{
"value": "production",
"displayValue": "Production"
},
{
"value": "stage",
"displayValue": "Stage (testing only)"
}
],
"simpleValueType": true,
"defaultValue": "production",
"help": "Use Stage, with your stage account ID, to test before going live. Switch to Production and your production account ID when you go live."
},
{
"type": "RADIO",
"name": "dataSource",
"displayName": "Order data source",
"radioItems": [
{
"value": "dataLayer",
"displayValue": "Auto-detect from GA4 ecommerce dataLayer (recommended)"
},
{
"value": "manual",
"displayValue": "Map fields manually"
}
],
"simpleValueType": true,
"defaultValue": "dataLayer"
},
{
"type": "TEXT",
"name": "transactionId",
"displayName": "Transaction ID",
"simpleValueType": true,
"enablingConditions": [
{
"paramName": "dataSource",
"paramValue": "manual",
"type": "EQUALS"
}
],
"valueValidators": [
{
"type": "NON_EMPTY",
"enablingConditions": [
{
"paramName": "dataSource",
"paramValue": "manual",
"type": "EQUALS"
}
]
}
],
"help": "Reference your transaction ID variable, e.g. {{DLV - ecommerce.transaction_id}}."
},
{
"type": "SIMPLE_TABLE",
"name": "orders",
"displayName": "Order line items",
"simpleTableColumns": [
{
"defaultValue": "",
"displayName": "SKU",
"name": "sku",
"type": "TEXT"
},
{
"defaultValue": "",
"displayName": "Price",
"name": "price",
"type": "TEXT"
},
{
"defaultValue": "1",
"displayName": "Quantity",
"name": "quantity",
"type": "TEXT"
}
],
"enablingConditions": [
{
"paramName": "dataSource",
"paramValue": "manual",
"type": "EQUALS"
}
],
"help": "Each row should reference a GTM variable resolving to a single value, or a hardcoded string for testing. Price is what the customer paid for one unit, after discounts, without tax or shipping."
},
{
"type": "TEXT",
"name": "couponCodes",
"displayName": "Coupon codes (optional)",
"simpleValueType": true,
"help": "A variable that resolves to an array of coupon codes, or a comma-separated string. Leave blank to read ecommerce.coupon and each item's coupon from the GA4 dataLayer (auto-detect mode only)."
},
{
"type": "TEXT",
"name": "newCustomer",
"displayName": "New customer (optional)",
"simpleValueType": true,
"help": "A variable that resolves to true or false. You decide what counts as a new customer. Leave blank to read new_customer from the dataLayer (auto-detect mode only). If the value is unknown, nothing is sent."
},
{
"type": "CHECKBOX",
"name": "enableLogging",
"checkboxText": "Enable console logging (debug only)",
"simpleValueType": true,
"defaultValue": false
}
]
___SANDBOXED_JS_FOR_WEB_TEMPLATE___
const injectScript = require('injectScript');
const createQueue = require('createQueue');
const copyFromDataLayer = require('copyFromDataLayer');
const logToConsole = require('logToConsole');
const makeString = require('makeString');
const getType = require('getType');
const log = (label, payload) => {
if (data.enableLogging) {
logToConsole('[Creatable]', label, payload);
}
};
const isSet = value => value !== undefined && value !== null && value !== '';
// Accepts an array of codes or a comma-separated string.
const toCodes = value => {
const type = getType(value);
if (type === 'array') return value;
if (type === 'string') return value.split(',');
return [];
};
const toNewCustomer = value => {
const text = makeString(value).trim().toLowerCase();
if (text === 'true' || text === '1') return true;
if (text === 'false' || text === '0') return false;
return undefined;
};
const tvpaPush = createQueue('_tvpa');
const isStage = data.environment === 'stage';
// 1) Config
const config = { li: makeString(data.accountId) };
if (isStage) {
config.logUrl = 'https://stage.tvpage.com/api/__tvpa.gif';
}
const cookieDomain = makeString(data.cookieDomain || '').trim();
if (cookieDomain) {
config.cookieDomain = cookieDomain;
}
tvpaPush(['config', config]);
log('config', config);
// 2) Build the order payload
let tid;
let orders = [];
let rawCoupons = [];
let rawNewCustomer;
if (data.dataSource === 'dataLayer') {
tid = copyFromDataLayer('ecommerce.transaction_id');
const items = copyFromDataLayer('ecommerce.items') || [];
orders = items.map(item => ({
sku: makeString(item.item_id || item.id || item.sku || ''),
price: makeString(item.price || '0'),
quantity: makeString(item.quantity || '1')
}));
rawCoupons = toCodes(copyFromDataLayer('ecommerce.coupon'));
items.forEach(item => {
rawCoupons = rawCoupons.concat(toCodes(item.coupon));
});
rawNewCustomer = copyFromDataLayer('new_customer');
} else {
tid = data.transactionId;
orders = (data.orders || []).map(row => ({
sku: makeString(row.sku),
price: makeString(row.price),
quantity: makeString(row.quantity || '1')
}));
}
if (isSet(data.couponCodes)) {
rawCoupons = toCodes(data.couponCodes);
}
if (isSet(data.newCustomer)) {
rawNewCustomer = data.newCustomer;
}
const coupons = [];
rawCoupons.forEach(code => {
const type = getType(code);
if (type !== 'string' && type !== 'number') return;
const trimmed = makeString(code).trim();
if (trimmed !== '' && coupons.indexOf(trimmed) < 0) {
coupons.push(trimmed);
}
});
const newCustomer = toNewCustomer(rawNewCustomer);
if (!tid || orders.length === 0) {
log('aborting — missing tid or empty orders', { tid: tid, orders: orders });
data.gtmOnFailure();
return;
}
// 3) Track
const payload = {
tid: makeString(tid),
orders: orders
};
if (coupons.length > 0) {
payload.coupons = coupons;
}
if (newCustomer !== undefined) {
payload.new_customer = newCustomer;
}
tvpaPush(['track', 'products', payload]);
log('track', payload);
// 4) Inject the Creatable library
const url = isStage ? 'https://a.tvpage.com/stage/tvpa.min.js' : 'https://a.tvpage.com/tvpa.min.js';
injectScript(url, data.gtmOnSuccess, data.gtmOnFailure, url);
___WEB_PERMISSIONS___
[
{
"instance": {
"key": {
"publicId": "inject_script",
"versionId": "1"
},
"param": [
{
"key": "urls",
"value": {
"type": 2,
"listItem": [
{
"type": 1,
"string": "https://a.tvpage.com/tvpa.min.js"
},
{
"type": 1,
"string": "https://a.tvpage.com/stage/tvpa.min.js"
}
]
}
}
]
},
"clientAnnotations": {
"isEditedByUser": true
},
"isRequired": true
},
{
"instance": {
"key": {
"publicId": "access_globals",
"versionId": "1"
},
"param": [
{
"key": "keys",
"value": {
"type": 2,
"listItem": [
{
"type": 3,
"mapKey": [
{"type": 1, "string": "key"},
{"type": 1, "string": "read"},
{"type": 1, "string": "write"},
{"type": 1, "string": "execute"}
],
"mapValue": [
{"type": 1, "string": "_tvpa"},
{"type": 8, "boolean": true},
{"type": 8, "boolean": true},
{"type": 8, "boolean": true}
]
}
]
}
}
]
},
"clientAnnotations": {
"isEditedByUser": true
},
"isRequired": true
},
{
"instance": {
"key": {
"publicId": "read_data_layer",
"versionId": "1"
},
"param": [
{
"key": "keyPatterns",
"value": {
"type": 2,
"listItem": [
{
"type": 1,
"string": "ecommerce.transaction_id"
},
{
"type": 1,
"string": "ecommerce.items"
},
{
"type": 1,
"string": "ecommerce.coupon"
},
{
"type": 1,
"string": "new_customer"
}
]
}
}
]
},
"clientAnnotations": {
"isEditedByUser": true
},
"isRequired": true
},
{
"instance": {
"key": {
"publicId": "logging",
"versionId": "1"
},
"param": [
{
"key": "environments",
"value": {
"type": 1,
"string": "debug"
}
}
]
},
"clientAnnotations": {
"isEditedByUser": true
},
"isRequired": true
}
]
___TESTS___
scenarios:
- name: GA4 mode sends coupons and new customer
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4, coupon: 'SAVE10'}],
'ecommerce.coupon': 'SAVE10, VIP',
'new_customer': true
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer'});
assertThat(pushes[1]).isEqualTo(['track', 'products', {
tid: 'ORDER-100045',
orders: [{sku: 'bb8100', price: '14.99', quantity: '4'}],
coupons: ['SAVE10', 'VIP'],
new_customer: true
}]);
assertApi('gtmOnSuccess').wasCalled();
- name: GA4 mode without coupons or new customer sends only tid and orders
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer'});
assertThat(pushes[1]).isEqualTo(['track', 'products', {
tid: 'ORDER-100045',
orders: [{sku: 'bb8100', price: '14.99', quantity: '4'}]
}]);
assertApi('gtmOnSuccess').wasCalled();
- name: Manual mode reads the coupon codes and new customer fields
code: |-
runCode({
accountId: '1759449',
environment: 'production',
dataSource: 'manual',
transactionId: 'ORDER-100045',
orders: [{sku: 'bb8100', price: '14.99', quantity: '4'}],
couponCodes: 'A,B',
newCustomer: 'false'
});
assertThat(pushes[1]).isEqualTo(['track', 'products', {
tid: 'ORDER-100045',
orders: [{sku: 'bb8100', price: '14.99', quantity: '4'}],
coupons: ['A', 'B'],
new_customer: false
}]);
assertApi('gtmOnSuccess').wasCalled();
- name: Unknown new customer value is left out
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}],
'new_customer': 'maybe'
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer'});
assertThat(pushes[1]).isEqualTo(['track', 'products', {
tid: 'ORDER-100045',
orders: [{sku: 'bb8100', price: '14.99', quantity: '4'}]
}]);
assertApi('gtmOnSuccess').wasCalled();
- name: Missing transaction ID fails without loading the library
code: |-
dataLayer = {
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer'});
assertApi('gtmOnFailure').wasCalled();
assertApi('injectScript').wasNotCalled();
- name: Stage loads the stage library and sends to the stage pixel
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'stage', dataSource: 'dataLayer'});
assertThat(pushes[0]).isEqualTo(['config', {li: '1759449', logUrl: 'https://stage.tvpage.com/api/__tvpa.gif'}]);
assertThat(injected).isEqualTo(['https://a.tvpage.com/stage/tvpa.min.js']);
assertApi('gtmOnSuccess').wasCalled();
- name: Production loads the production library with no logUrl
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer'});
assertThat(pushes[0]).isEqualTo(['config', {li: '1759449'}]);
assertThat(injected).isEqualTo(['https://a.tvpage.com/tvpa.min.js']);
assertApi('gtmOnSuccess').wasCalled();
- name: Cookie domain is passed through when set
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer', cookieDomain: ' us.example.com '});
assertThat(pushes[0]).isEqualTo(['config', {li: '1759449', cookieDomain: 'us.example.com'}]);
assertApi('gtmOnSuccess').wasCalled();
- name: Cookie domain is left out when blank
code: |-
dataLayer = {
'ecommerce.transaction_id': 'ORDER-100045',
'ecommerce.items': [{item_id: 'bb8100', price: 14.99, quantity: 4}]
};
runCode({accountId: '1759449', environment: 'production', dataSource: 'dataLayer', cookieDomain: ' '});
assertThat(pushes[0]).isEqualTo(['config', {li: '1759449'}]);
assertApi('gtmOnSuccess').wasCalled();
setup: |-
let dataLayer = {};
const pushes = [];
const injected = [];
mock('copyFromDataLayer', key => dataLayer[key]);
mock('createQueue', name => args => {
pushes.push(args);
});
mock('injectScript', (url, onSuccess, onFailure) => {
injected.push(url);
onSuccess();
});