Skip to content

Commit 112da9c

Browse files
committed
gtm datalayer events support
1 parent 3932652 commit 112da9c

File tree

14 files changed

+424
-17
lines changed

14 files changed

+424
-17
lines changed

README.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,19 @@ See [schema.json](schema.json) for a JSON Schema of the output.
101101
102102
| Library | JavaScript/TypeScript | Python | Ruby | Go |
103103
|---------|:---------------------:|:------:|:----:|:--:|
104-
| Google Analytics | ✅ | ❌ | ❌ | ❌ |
105-
| Segment | ✅ | ✅ | ✅ | ✅ |
106-
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
107-
| Amplitude | ✅ | ✅ | ❌ | ✅ |
108-
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
109-
| mParticle | ✅ | ❌ | ❌ | ❌ |
110-
| PostHog | ✅ | ✅ | ✅ | ✅ |
111-
| Pendo | ✅ | ❌ | ❌ | ❌ |
112-
| Heap | ✅ | ❌ | ❌ | ❌ |
113-
| Snowplow | ✅ | ✅ | ✅ | ✅ |
114-
| Datadog RUM | ✅ | ❌ | ❌ | ❌ |
115-
| Custom Function | ✅ | ✅ | ✅ | ✅ |
104+
| Google Analytics | ✅ | ❌ | ❌ | ❌ |
105+
| Google Tag Manager | ✅ | ❌ | ❌ | ❌ |
106+
| Segment | ✅ | ✅ | ✅ | ✅ |
107+
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
108+
| Amplitude | ✅ | ✅ | ❌ | ✅ |
109+
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
110+
| mParticle | ✅ | ❌ | ❌ | ❌ |
111+
| PostHog | ✅ | ✅ | ✅ | ✅ |
112+
| Pendo | ✅ | ❌ | ❌ | ❌ |
113+
| Heap | ✅ | ❌ | ❌ | ❌ |
114+
| Snowplow | ✅ | ✅ | ✅ | ✅ |
115+
| Datadog RUM | ✅ | ❌ | ❌ | ❌ |
116+
| Custom Function | ✅ | ✅ | ✅ | ✅ |
116117
117118
✳️ Rudderstack's SDKs often use the same format as Segment, so Rudderstack events may be detected as Segment events.
118119
@@ -130,6 +131,24 @@ See [schema.json](schema.json) for a JSON Schema of the output.
130131
```
131132
</details>
132133

134+
<details>
135+
<summary>Google Tag Manager</summary>
136+
137+
**JavaScript/TypeScript**
138+
```js
139+
dataLayer.push({
140+
event: '<event_name>',
141+
'<property_name>': '<property_value>'
142+
});
143+
144+
// Or via window
145+
window.dataLayer.push({
146+
event: '<event_name>',
147+
'<property_name>': '<property_value>'
148+
});
149+
```
150+
</details>
151+
133152
<details>
134153
<summary>Segment</summary>
135154

schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"heap",
6969
"snowplow",
7070
"datadog",
71+
"gtm",
7172
"custom",
7273
"unknown"
7374
],

src/analyze/javascript/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ const ANALYTICS_PROVIDERS = {
8181
objectNames: ['datadogRum', 'DD_RUM'],
8282
methodName: 'addAction',
8383
type: 'member'
84+
},
85+
GOOGLE_TAG_MANAGER: {
86+
name: 'gtm',
87+
objectNames: ['dataLayer'],
88+
methodName: 'push',
89+
type: 'member'
8490
}
8591
};
8692

src/analyze/javascript/extractors/event-extractor.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const EXTRACTION_STRATEGIES = {
2020
googleanalytics: extractGoogleAnalyticsEvent,
2121
snowplow: extractSnowplowEvent,
2222
mparticle: extractMparticleEvent,
23+
gtm: extractGTMEvent,
2324
custom: extractCustomEvent,
2425
default: extractDefaultEvent
2526
};
@@ -105,6 +106,43 @@ function extractMparticleEvent(node, constantMap) {
105106
return { eventName, propertiesNode };
106107
}
107108

109+
/**
110+
* Extracts Google Tag Manager event data
111+
* @param {Object} node - CallExpression node
112+
* @param {Object} constantMap - Collected constant map
113+
* @returns {EventData}
114+
*/
115+
function extractGTMEvent(node, constantMap) {
116+
if (!node.arguments || node.arguments.length === 0) {
117+
return { eventName: null, propertiesNode: null };
118+
}
119+
120+
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
121+
const firstArg = node.arguments[0];
122+
123+
if (firstArg.type !== NODE_TYPES.OBJECT_EXPRESSION) {
124+
return { eventName: null, propertiesNode: null };
125+
}
126+
127+
// Find the 'event' property
128+
const eventProperty = findPropertyByKey(firstArg, 'event');
129+
if (!eventProperty) {
130+
return { eventName: null, propertiesNode: null };
131+
}
132+
133+
const eventName = getStringValue(eventProperty.value, constantMap);
134+
135+
// Create a modified properties node without the 'event' property
136+
const modifiedPropertiesNode = {
137+
...firstArg,
138+
properties: firstArg.properties.filter(prop =>
139+
prop.key && (prop.key.name !== 'event' && prop.key.value !== 'event')
140+
)
141+
};
142+
143+
return { eventName, propertiesNode: modifiedPropertiesNode };
144+
}
145+
108146
/**
109147
* Default event extraction for standard providers
110148
* @param {Object} node - CallExpression node

src/analyze/typescript/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ const ANALYTICS_PROVIDERS = {
8181
objectNames: ['datadogRum', 'DD_RUM'],
8282
methodName: 'addAction',
8383
type: 'member'
84+
},
85+
GOOGLE_TAG_MANAGER: {
86+
name: 'gtm',
87+
objectNames: ['dataLayer'],
88+
methodName: 'push',
89+
type: 'member'
8490
}
8591
};
8692

src/analyze/typescript/extractors/event-extractor.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const EXTRACTION_STRATEGIES = {
2121
googleanalytics: extractGoogleAnalyticsEvent,
2222
snowplow: extractSnowplowEvent,
2323
mparticle: extractMparticleEvent,
24+
gtm: extractGTMEvent,
2425
custom: extractCustomEvent,
2526
default: extractDefaultEvent
2627
};
@@ -126,6 +127,60 @@ function extractMparticleEvent(node, checker, sourceFile) {
126127
return { eventName, propertiesNode };
127128
}
128129

130+
/**
131+
* Extracts Google Tag Manager event data
132+
* @param {Object} node - CallExpression node
133+
* @param {Object} checker - TypeScript type checker
134+
* @param {Object} sourceFile - TypeScript source file
135+
* @returns {EventData}
136+
*/
137+
function extractGTMEvent(node, checker, sourceFile) {
138+
if (!node.arguments || node.arguments.length === 0) {
139+
return { eventName: null, propertiesNode: null };
140+
}
141+
142+
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
143+
const firstArg = node.arguments[0];
144+
145+
if (!ts.isObjectLiteralExpression(firstArg)) {
146+
return { eventName: null, propertiesNode: null };
147+
}
148+
149+
// Find the 'event' property
150+
const eventProperty = findPropertyByKey(firstArg, 'event');
151+
if (!eventProperty) {
152+
return { eventName: null, propertiesNode: null };
153+
}
154+
155+
const eventName = getStringValue(eventProperty.initializer, checker, sourceFile);
156+
157+
// Create a modified properties node without the 'event' property
158+
const modifiedProperties = firstArg.properties.filter(prop => {
159+
if (ts.isPropertyAssignment(prop) && prop.name) {
160+
if (ts.isIdentifier(prop.name)) {
161+
return prop.name.escapedText !== 'event';
162+
}
163+
if (ts.isStringLiteral(prop.name)) {
164+
return prop.name.text !== 'event';
165+
}
166+
}
167+
return true;
168+
});
169+
170+
// Create a synthetic object literal with the filtered properties
171+
const modifiedPropertiesNode = ts.factory.createObjectLiteralExpression(modifiedProperties);
172+
173+
// Copy source positions for proper analysis
174+
if (firstArg.pos !== undefined) {
175+
modifiedPropertiesNode.pos = firstArg.pos;
176+
}
177+
if (firstArg.end !== undefined) {
178+
modifiedPropertiesNode.end = firstArg.end;
179+
}
180+
181+
return { eventName, propertiesNode: modifiedPropertiesNode };
182+
}
183+
129184
/**
130185
* Custom extraction
131186
* @param {Object} node - CallExpression node

tests/analyzeJavaScript.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test.describe('analyzeJsFile', () => {
1616
// Sort events by line number for consistent ordering
1717
events.sort((a, b) => a.line - b.line);
1818

19-
assert.strictEqual(events.length, 15);
19+
assert.strictEqual(events.length, 19);
2020

2121
// Test Google Analytics event
2222
const gaEvent = events.find(e => e.eventName === 'purchase' && e.source === 'googleanalytics');
@@ -232,7 +232,7 @@ test.describe('analyzeJsFile', () => {
232232
const events = analyzeJsFile(testFilePath, null);
233233

234234
// Should find all events except the custom one
235-
assert.strictEqual(events.length, 14);
235+
assert.strictEqual(events.length, 18);
236236
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
237237
});
238238

tests/analyzeTypeScript.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ test.describe('analyzeTsFile', () => {
3737
// Sort events by line number for consistent ordering
3838
events.sort((a, b) => a.line - b.line);
3939

40-
assert.strictEqual(events.length, 23);
40+
assert.strictEqual(events.length, 25);
4141

4242
// Test Google Analytics event
4343
const gaEvent = events.find(e => e.eventName === 'order_completed' && e.source === 'googleanalytics');
@@ -405,7 +405,7 @@ test.describe('analyzeTsFile', () => {
405405
const events = analyzeTsFile(testFilePath, program, null);
406406

407407
// Should find all events except the custom ones
408-
assert.strictEqual(events.length, 16);
408+
assert.strictEqual(events.length, 18);
409409
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
410410
});
411411

tests/fixtures/javascript/main.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,51 @@ mixpanel.track(TRACKING_EVENTS_FROZEN.ECOMMERCE_PURCHASE, {
211211
total: 99.99,
212212
items: ['sku_1', 'sku_2']
213213
});
214+
215+
// -----------------------------------------------------------------------------
216+
// Google Tag Manager (GTM) tracking examples
217+
// -----------------------------------------------------------------------------
218+
219+
// GTM example 1: window.dataLayer.push
220+
window.dataLayer.push({
221+
'event': 'formSubmission',
222+
'formId': 'contactForm',
223+
'formLocation': 'footer'
224+
});
225+
226+
// GTM example 2: dataLayer.push (without window)
227+
dataLayer.push({
228+
'event': 'userRegistration',
229+
'userId': 'user123',
230+
'source': 'organic',
231+
'plan': 'premium'
232+
});
233+
234+
// GTM example 3: complex properties
235+
window.dataLayer.push({
236+
'event': 'ecommerce_purchase',
237+
'transactionId': 'txn_123',
238+
'value': 99.99,
239+
'currency': 'USD',
240+
'items': [
241+
{
242+
'item_id': 'sku_001',
243+
'item_name': 'Product A',
244+
'price': 49.99
245+
},
246+
{
247+
'item_id': 'sku_002',
248+
'item_name': 'Product B',
249+
'price': 50.00
250+
}
251+
]
252+
});
253+
254+
function gtmTestFunction() {
255+
// GTM example 4: inside a function
256+
dataLayer.push({
257+
'event': 'buttonClick',
258+
'buttonText': 'Subscribe Now',
259+
'location': 'header'
260+
});
261+
}

tests/fixtures/javascript/tracking-schema-javascript.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,55 @@ events:
316316
type: string
317317
success:
318318
type: boolean
319+
formSubmission:
320+
implementations:
321+
- path: main.js
322+
line: 220
323+
function: global
324+
destination: gtm
325+
properties:
326+
formId:
327+
type: string
328+
formLocation:
329+
type: string
330+
userRegistration:
331+
implementations:
332+
- path: main.js
333+
line: 227
334+
function: global
335+
destination: gtm
336+
properties:
337+
userId:
338+
type: string
339+
source:
340+
type: string
341+
plan:
342+
type: string
343+
ecommerce_purchase:
344+
implementations:
345+
- path: main.js
346+
line: 235
347+
function: global
348+
destination: gtm
349+
properties:
350+
transactionId:
351+
type: string
352+
value:
353+
type: number
354+
currency:
355+
type: string
356+
items:
357+
type: array
358+
items:
359+
type: object
360+
buttonClick:
361+
implementations:
362+
- path: main.js
363+
line: 256
364+
function: gtmTestFunction
365+
destination: gtm
366+
properties:
367+
buttonText:
368+
type: string
369+
location:
370+
type: string

0 commit comments

Comments
 (0)