Skill Tracer
Skills › Marketing Skills › conversion-tracking › detect-tally.json

detect-tally.json

122 lines · skills/conversion-tracking/assets/gtm-recipes/detect/detect-tally.json at 88c79f6

1{
2 "exportFormatVersion": 2,
3 "exportTime": "2026-07-02 00:00:00",
4 "containerVersion": {
5 "path": "accounts/0/containers/0/versions/0",
6 "accountId": "0",
7 "containerId": "0",
8 "containerVersionId": "0",
9 "container": {
10 "path": "accounts/0/containers/0",
11 "accountId": "0",
12 "containerId": "0",
13 "name": "Conversion Detection - Tally",
14 "publicId": "GTM-XXXXXXX",
15 "usageContext": [
16 "WEB"
17 ],
18 "fingerprint": "0",
19 "tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/0/containers/0/workspaces/0"
20 },
21 "tag": [
22 {
23 "accountId": "0",
24 "containerId": "0",
25 "tagId": "1",
26 "name": "Conversion Detection - Tally Submission Listener",
27 "type": "html",
28 "parameter": [
29 {
30 "type": "TEMPLATE",
31 "key": "html",
32 "value": "<script>\n/* Tally Submission Listener. Reads only the envelope of Tally's\n * own submission message; it does not decode or read answer values and never\n * pushes answers. */\n(function () {\n 'use strict';\n if (window.__ctDetectTally || window.__ctTallyMethod2Installed) return;\n window.__ctDetectTally = true;\n // Keep the historical Method 2 marker as a one-way compatibility alias.\n // The separate Method 2 adapter has its own marker and can still register\n // after this core is already installed.\n window.__ctTallyMethod2Installed = true;\n window.dataLayer = window.dataLayer || [];\n\n function isWs(c) { return c === ' ' || c === '\\n' || c === '\\r' || c === '\\t'; }\n function skipWs(s, p) { while (p.i < s.length && isWs(s.charAt(p.i))) p.i++; }\n function stringEnd(s, p, decode) {\n if (s.charAt(p.i) !== '\"') throw new Error('string');\n var start = p.i++;\n while (p.i < s.length) {\n var c = s.charAt(p.i++);\n if (c === '\"') {\n if (!decode) return null;\n // Decode only object keys and allowlisted envelope metadata strings.\n return JSON.parse(s.slice(start, p.i));\n }\n if (c.charCodeAt(0) < 0x20) throw new Error('control');\n if (c === '\\\\') {\n if (p.i >= s.length) throw new Error('escape end');\n var esc = s.charAt(p.i++);\n if ('\"\\\\/bfnrt'.indexOf(esc) !== -1) continue;\n if (esc !== 'u' || !/^[0-9a-fA-F]{4}$/.test(s.slice(p.i, p.i + 4))) throw new Error('escape');\n p.i += 4;\n }\n }\n throw new Error('unterminated');\n }\n function skipValue(s, p, depth) {\n if (depth > 32) throw new Error('depth');\n skipWs(s, p);\n var c = s.charAt(p.i);\n if (c === '\"') { stringEnd(s, p, false); return; }\n if (c === '{') {\n p.i++; skipWs(s, p); if (s.charAt(p.i) === '}') { p.i++; return; }\n while (p.i < s.length) {\n skipWs(s, p); stringEnd(s, p, false); skipWs(s, p);\n if (s.charAt(p.i++) !== ':') throw new Error('colon');\n skipValue(s, p, depth + 1); skipWs(s, p);\n var sep = s.charAt(p.i++); if (sep === '}') return; if (sep !== ',') throw new Error('object');\n }\n throw new Error('object end');\n }\n if (c === '[') {\n p.i++; skipWs(s, p); if (s.charAt(p.i) === ']') { p.i++; return; }\n while (p.i < s.length) {\n skipValue(s, p, depth + 1); skipWs(s, p);\n var a = s.charAt(p.i++); if (a === ']') return; if (a !== ',') throw new Error('array');\n }\n throw new Error('array end');\n }\n var begin = p.i;\n while (p.i < s.length && !isWs(s.charAt(p.i)) && ',}]'.indexOf(s.charAt(p.i)) === -1) p.i++;\n if (p.i === begin) throw new Error('value');\n var token = s.slice(begin, p.i);\n if (token !== 'true' && token !== 'false' && token !== 'null' &&\n !/^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(token)) throw new Error('primitive');\n }\n function readStringValue(s, p) { skipWs(s, p); return stringEnd(s, p, true); }\n function readMetadataObject(s, p, wanted, out) {\n skipWs(s, p); if (s.charAt(p.i++) !== '{') throw new Error('metadata object');\n skipWs(s, p); if (s.charAt(p.i) === '}') { p.i++; return; }\n while (p.i < s.length) {\n skipWs(s, p); var key = stringEnd(s, p, true); skipWs(s, p);\n if (s.charAt(p.i++) !== ':') throw new Error('metadata colon');\n if (wanted[key]) { out[key] = readStringValue(s, p); }\n else if (key === 'fields') { skipWs(s, p); if (s.charAt(p.i) !== '[') throw new Error('fields shape'); skipValue(s, p, 0); out.fieldsArray = true; }\n else skipValue(s, p, 0);\n skipWs(s, p); var sep = s.charAt(p.i++); if (sep === '}') return; if (sep !== ',') throw new Error('metadata separator');\n }\n throw new Error('metadata end');\n }\n function readEnvelope(s) {\n if (typeof s !== 'string' || s.length > 1048576) return null;\n var p = { i: 0 }, out = {}, payload = {};\n try {\n skipWs(s, p); if (s.charAt(p.i++) !== '{') return null;\n skipWs(s, p); if (s.charAt(p.i) === '}') return null;\n while (p.i < s.length) {\n skipWs(s, p); var key = stringEnd(s, p, true); skipWs(s, p);\n if (s.charAt(p.i++) !== ':') return null;\n if (key === 'event') out.event = readStringValue(s, p);\n else if (key === 'payload') readMetadataObject(s, p, { id: true, formId: true }, payload);\n else skipValue(s, p, 0);\n skipWs(s, p); var sep = s.charAt(p.i++); if (sep === '}') break; if (sep !== ',') return null;\n }\n skipWs(s, p); if (p.i !== s.length) return null;\n if (out.event !== 'Tally.FormSubmitted') return null;\n if (!payload.id || !payload.formId || !payload.fieldsArray || !/^[-A-Za-z0-9_]{1,128}$/.test(payload.id) || !/^[-A-Za-z0-9_]{1,128}$/.test(payload.formId)) return null;\n return { id: payload.id, formId: payload.formId };\n } catch (e) { return null; }\n }\n function isTallyOrigin(origin) {\n return typeof origin === 'string' && /^https:\\/\\/tally\\.so(?::443)?$/i.test(origin);\n }\n // Bind only fixed-host iframe paths using CSS attribute selectors. Do not\n // retrieve src or data-tally-src: either may contain prefilled respondent\n // data in its query. Only a materialized iframe src is supported here; a\n // deferred attribute without a loaded src fails closed.\n function frameFormId(frame, expectedFormId) {\n if (!/^[-A-Za-z0-9_]{1,128}$/.test(expectedFormId || '') ||\n !frame || typeof frame.matches !== 'function') return '';\n var found = false;\n var modes = ['embed', 'r', 'popup'];\n for (var i = 0; i < modes.length; i++) {\n var base = 'https://tally.so/' + modes[i] + '/' + expectedFormId;\n var selectors = [\n 'iframe[src=\"' + base + '\"]',\n 'iframe[src^=\"' + base + '?\"]',\n 'iframe[src^=\"' + base + '#\"]'\n ];\n for (var j = 0; j < selectors.length; j++) {\n try { if (frame.matches(selectors[j])) { found = true; break; } }\n catch (e) { return ''; }\n }\n if (found) return expectedFormId;\n }\n return '';\n }\n function renderedFrameFormId(source, expectedFormId) {\n if (!source || source === window) return '';\n var frames;\n try { frames = document.getElementsByTagName('iframe'); } catch (e) { return ''; }\n for (var i = 0; frames && i < frames.length; i++) {\n try {\n if (frames[i].contentWindow === source) {\n // The event carries only a bounded public form ID. The selector tests\n // the fixed-host path and exact delimiter without exposing the URL.\n return frameFormId(frames[i], expectedFormId);\n }\n } catch (e) {}\n }\n return '';\n }\n var seen = Object.create(null);\n window.addEventListener('message', function (event) {\n try {\n if (!event || !isTallyOrigin(event.origin)) return;\n var verdict = readEnvelope(event.data);\n if (!verdict) return;\n var configuredFormId = renderedFrameFormId(event.source, verdict.formId);\n if (!configuredFormId || verdict.formId !== configuredFormId) return;\n var key = verdict.formId + ':' + verdict.id;\n if (seen[key]) return;\n seen[key] = true;\n var handled = false;\n var adapter = window.__ctTallyMethod2Handler;\n if (typeof adapter === 'function') {\n try { handled = adapter(event, verdict) === true; } catch (e) { handled = false; }\n }\n if (!handled) window.dataLayer.push({ event: 'tally_form_submitted', form_id: verdict.formId, submission_id: verdict.id });\n } catch (e) {}\n }, false);\n})();\n</script>"
33 },
34 {
35 "type": "BOOLEAN",
36 "key": "supportDocumentWrite",
37 "value": "false"
38 }
39 ],
40 "firingTriggerId": [
41 "2147479553"
42 ],
43 "tagFiringOption": "ONCE_PER_EVENT",
44 "monitoringMetadata": {
45 "type": "MAP"
46 },
47 "consentSettings": {
48 "consentStatus": "NOT_SET"
49 }
50 }
51 ],
52 "trigger": [
53 {
54 "accountId": "0",
55 "containerId": "0",
56 "triggerId": "2",
57 "name": "CE - Tally Form Submitted",
58 "type": "CUSTOM_EVENT",
59 "customEventFilter": [
60 {
61 "type": "EQUALS",
62 "parameter": [
63 {
64 "type": "TEMPLATE",
65 "key": "arg0",
66 "value": "{{_event}}"
67 },
68 {
69 "type": "TEMPLATE",
70 "key": "arg1",
71 "value": "tally_form_submitted"
72 }
73 ]
74 }
75 ],
76 "filter": []
77 }
78 ],
79 "variable": [
80 {
81 "accountId": "0",
82 "containerId": "0",
83 "variableId": "1",
84 "name": "DLV - Tally Form ID",
85 "type": "v",
86 "parameter": [
87 {
88 "type": "TEMPLATE",
89 "key": "name",
90 "value": "form_id"
91 },
92 {
93 "type": "INTEGER",
94 "key": "dataLayerVersion",
95 "value": "2"
96 }
97 ]
98 },
99 {
100 "accountId": "0",
101 "containerId": "0",
102 "variableId": "3",
103 "name": "DLV - Tally Form Name",
104 "type": "v",
105 "parameter": [
106 {
107 "type": "TEMPLATE",
108 "key": "name",
109 "value": "form_name"
110 },
111 {
112 "type": "INTEGER",
113 "key": "dataLayerVersion",
114 "value": "2"
115 }
116 ]
117 }
118 ],
119 "fingerprint": "0",
120 "tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/0/containers/0/workspaces/0"
121 }