Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,19 @@ See [schema.json](schema.json) for a JSON Schema of the output.

| Library | JavaScript/TypeScript | Python | Ruby | Go |
|---------|:---------------------:|:------:|:----:|:--:|
| Google Analytics | ✅ | ❌ | ❌ | ❌ |
| Segment | ✅ | ✅ | ✅ | ✅ |
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
| Amplitude | ✅ | ✅ | ❌ | ✅ |
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
| mParticle | ✅ | ❌ | ❌ | ❌ |
| PostHog | ✅ | ✅ | ✅ | ✅ |
| Pendo | ✅ | ❌ | ❌ | ❌ |
| Heap | ✅ | ❌ | ❌ | ❌ |
| Snowplow | ✅ | ✅ | ✅ | ✅ |
| Datadog RUM | ✅ | ❌ | ❌ | ❌ |
| Custom Function | ✅ | ✅ | ✅ | ✅ |
| Google Analytics | ✅ | ❌ | ❌ | ❌ |
| Google Tag Manager | ✅ | ❌ | ❌ | ❌ |
| Segment | ✅ | ✅ | ✅ | ✅ |
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
| Amplitude | ✅ | ✅ | ❌ | ✅ |
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
| mParticle | ✅ | ❌ | ❌ | ❌ |
| PostHog | ✅ | ✅ | ✅ | ✅ |
| Pendo | ✅ | ❌ | ❌ | ❌ |
| Heap | ✅ | ❌ | ❌ | ❌ |
| Snowplow | ✅ | ✅ | ✅ | ✅ |
| Datadog RUM | ✅ | ❌ | ❌ | ❌ |
| Custom Function | ✅ | ✅ | ✅ | ✅ |

✳️ Rudderstack's SDKs often use the same format as Segment, so Rudderstack events may be detected as Segment events.

Expand All @@ -130,6 +131,24 @@ See [schema.json](schema.json) for a JSON Schema of the output.
```
</details>

<details>
<summary>Google Tag Manager</summary>

**JavaScript/TypeScript**
```js
dataLayer.push({
event: '<event_name>',
'<property_name>': '<property_value>'
});

// Or via window
window.dataLayer.push({
event: '<event_name>',
'<property_name>': '<property_value>'
});
```
</details>

<details>
<summary>Segment</summary>

Expand Down
1 change: 1 addition & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"heap",
"snowplow",
"datadog",
"gtm",
"custom",
"unknown"
],
Expand Down
6 changes: 6 additions & 0 deletions src/analyze/javascript/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ const ANALYTICS_PROVIDERS = {
objectNames: ['datadogRum', 'DD_RUM'],
methodName: 'addAction',
type: 'member'
},
GOOGLE_TAG_MANAGER: {
name: 'gtm',
objectNames: ['dataLayer'],
methodName: 'push',
type: 'member'
}
};

Expand Down
38 changes: 38 additions & 0 deletions src/analyze/javascript/extractors/event-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const EXTRACTION_STRATEGIES = {
googleanalytics: extractGoogleAnalyticsEvent,
snowplow: extractSnowplowEvent,
mparticle: extractMparticleEvent,
gtm: extractGTMEvent,
custom: extractCustomEvent,
default: extractDefaultEvent
};
Expand Down Expand Up @@ -105,6 +106,43 @@ function extractMparticleEvent(node, constantMap) {
return { eventName, propertiesNode };
}

/**
* Extracts Google Tag Manager event data
* @param {Object} node - CallExpression node
* @param {Object} constantMap - Collected constant map
* @returns {EventData}
*/
function extractGTMEvent(node, constantMap) {
if (!node.arguments || node.arguments.length === 0) {
return { eventName: null, propertiesNode: null };
}

// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
const firstArg = node.arguments[0];

if (firstArg.type !== NODE_TYPES.OBJECT_EXPRESSION) {
return { eventName: null, propertiesNode: null };
}

// Find the 'event' property
const eventProperty = findPropertyByKey(firstArg, 'event');
if (!eventProperty) {
return { eventName: null, propertiesNode: null };
}

const eventName = getStringValue(eventProperty.value, constantMap);

// Create a modified properties node without the 'event' property
const modifiedPropertiesNode = {
...firstArg,
properties: firstArg.properties.filter(prop =>
prop.key && (prop.key.name !== 'event' && prop.key.value !== 'event')
)
};

return { eventName, propertiesNode: modifiedPropertiesNode };
}

/**
* Default event extraction for standard providers
* @param {Object} node - CallExpression node
Expand Down
6 changes: 6 additions & 0 deletions src/analyze/typescript/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ const ANALYTICS_PROVIDERS = {
objectNames: ['datadogRum', 'DD_RUM'],
methodName: 'addAction',
type: 'member'
},
GOOGLE_TAG_MANAGER: {
name: 'gtm',
objectNames: ['dataLayer'],
methodName: 'push',
type: 'member'
}
};

Expand Down
55 changes: 55 additions & 0 deletions src/analyze/typescript/extractors/event-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const EXTRACTION_STRATEGIES = {
googleanalytics: extractGoogleAnalyticsEvent,
snowplow: extractSnowplowEvent,
mparticle: extractMparticleEvent,
gtm: extractGTMEvent,
custom: extractCustomEvent,
default: extractDefaultEvent
};
Expand Down Expand Up @@ -126,6 +127,60 @@ function extractMparticleEvent(node, checker, sourceFile) {
return { eventName, propertiesNode };
}

/**
* Extracts Google Tag Manager event data
* @param {Object} node - CallExpression node
* @param {Object} checker - TypeScript type checker
* @param {Object} sourceFile - TypeScript source file
* @returns {EventData}
*/
function extractGTMEvent(node, checker, sourceFile) {
if (!node.arguments || node.arguments.length === 0) {
return { eventName: null, propertiesNode: null };
}

// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
const firstArg = node.arguments[0];

if (!ts.isObjectLiteralExpression(firstArg)) {
return { eventName: null, propertiesNode: null };
}

// Find the 'event' property
const eventProperty = findPropertyByKey(firstArg, 'event');
if (!eventProperty) {
return { eventName: null, propertiesNode: null };
}

const eventName = getStringValue(eventProperty.initializer, checker, sourceFile);

// Create a modified properties node without the 'event' property
const modifiedProperties = firstArg.properties.filter(prop => {
if (ts.isPropertyAssignment(prop) && prop.name) {
if (ts.isIdentifier(prop.name)) {
return prop.name.escapedText !== 'event';
}
if (ts.isStringLiteral(prop.name)) {
return prop.name.text !== 'event';
}
}
return true;
});

// Create a synthetic object literal with the filtered properties
const modifiedPropertiesNode = ts.factory.createObjectLiteralExpression(modifiedProperties);

// Copy source positions for proper analysis
if (firstArg.pos !== undefined) {
modifiedPropertiesNode.pos = firstArg.pos;
}
if (firstArg.end !== undefined) {
modifiedPropertiesNode.end = firstArg.end;
}

return { eventName, propertiesNode: modifiedPropertiesNode };
}

/**
* Custom extraction
* @param {Object} node - CallExpression node
Expand Down
4 changes: 2 additions & 2 deletions tests/analyzeJavaScript.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test.describe('analyzeJsFile', () => {
// Sort events by line number for consistent ordering
events.sort((a, b) => a.line - b.line);

assert.strictEqual(events.length, 15);
assert.strictEqual(events.length, 19);

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

// Should find all events except the custom one
assert.strictEqual(events.length, 14);
assert.strictEqual(events.length, 18);
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
});

Expand Down
4 changes: 2 additions & 2 deletions tests/analyzeTypeScript.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test.describe('analyzeTsFile', () => {
// Sort events by line number for consistent ordering
events.sort((a, b) => a.line - b.line);

assert.strictEqual(events.length, 23);
assert.strictEqual(events.length, 25);

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

// Should find all events except the custom ones
assert.strictEqual(events.length, 16);
assert.strictEqual(events.length, 18);
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
});

Expand Down
48 changes: 48 additions & 0 deletions tests/fixtures/javascript/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,51 @@ mixpanel.track(TRACKING_EVENTS_FROZEN.ECOMMERCE_PURCHASE, {
total: 99.99,
items: ['sku_1', 'sku_2']
});

// -----------------------------------------------------------------------------
// Google Tag Manager (GTM) tracking examples
// -----------------------------------------------------------------------------

// GTM example 1: window.dataLayer.push
window.dataLayer.push({
'event': 'formSubmission',
'formId': 'contactForm',
'formLocation': 'footer'
});

// GTM example 2: dataLayer.push (without window)
dataLayer.push({
'event': 'userRegistration',
'userId': 'user123',
'source': 'organic',
'plan': 'premium'
});

// GTM example 3: complex properties
window.dataLayer.push({
'event': 'ecommerce_purchase',
'transactionId': 'txn_123',
'value': 99.99,
'currency': 'USD',
'items': [
{
'item_id': 'sku_001',
'item_name': 'Product A',
'price': 49.99
},
{
'item_id': 'sku_002',
'item_name': 'Product B',
'price': 50.00
}
]
});

function gtmTestFunction() {
// GTM example 4: inside a function
dataLayer.push({
'event': 'buttonClick',
'buttonText': 'Subscribe Now',
'location': 'header'
});
}
52 changes: 52 additions & 0 deletions tests/fixtures/javascript/tracking-schema-javascript.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,55 @@ events:
type: string
success:
type: boolean
formSubmission:
implementations:
- path: main.js
line: 220
function: global
destination: gtm
properties:
formId:
type: string
formLocation:
type: string
userRegistration:
implementations:
- path: main.js
line: 227
function: global
destination: gtm
properties:
userId:
type: string
source:
type: string
plan:
type: string
ecommerce_purchase:
implementations:
- path: main.js
line: 235
function: global
destination: gtm
properties:
transactionId:
type: string
value:
type: number
currency:
type: string
items:
type: array
items:
type: object
buttonClick:
implementations:
- path: main.js
line: 256
function: gtmTestFunction
destination: gtm
properties:
buttonText:
type: string
location:
type: string
Loading