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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ See [schema.json](schema.json) for a JSON Schema of the output.
| 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 Down Expand Up @@ -375,6 +376,27 @@ See [schema.json](schema.json) for a JSON Schema of the output.

</details>

<details>
<summary>Datadog RUM</summary>

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

// Or via window
window.DD_RUM.addAction('<event_name>', {
'<property_name>': '<property_value>'
});

// Or via global DD_RUM
DD_RUM.addAction('<event_name>', {
'<property_name>': '<property_value>'
});
```
</details>

<details>
<summary>Snowplow (Structured Events)</summary>

Expand Down
1 change: 1 addition & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"pendo",
"heap",
"snowplow",
"datadog",
"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 @@ -75,6 +75,12 @@ const ANALYTICS_PROVIDERS = {
name: 'googleanalytics',
functionName: 'gtag',
type: 'function'
},
DATADOG_RUM: {
name: 'datadog',
objectNames: ['datadogRum', 'DD_RUM'],
methodName: 'addAction',
type: 'member'
}
};

Expand Down
11 changes: 10 additions & 1 deletion src/analyze/javascript/detectors/analytics-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,17 @@ function detectMemberBasedProvider(node) {
return 'unknown';
}

const objectName = node.callee.object.name;
const methodName = node.callee.property.name;
let objectName = node.callee.object.name;

// Handle nested member expressions like window.DD_RUM.addAction
if (!objectName && node.callee.object.type === NODE_TYPES.MEMBER_EXPRESSION) {
// For window.DD_RUM.addAction, we want to check if it matches DD_RUM.addAction pattern
const nestedObjectName = node.callee.object.property.name;
if (nestedObjectName) {
objectName = nestedObjectName;
}
}

if (!objectName || !methodName) {
return 'unknown';
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 @@ -75,6 +75,12 @@ const ANALYTICS_PROVIDERS = {
name: 'googleanalytics',
functionName: 'gtag',
type: 'function'
},
DATADOG_RUM: {
name: 'datadog',
objectNames: ['datadogRum', 'DD_RUM'],
methodName: 'addAction',
type: 'member'
}
};

Expand Down
11 changes: 10 additions & 1 deletion src/analyze/typescript/detectors/analytics-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,17 @@ function detectMemberBasedProvider(node) {
return 'unknown';
}

const objectName = node.expression.expression?.escapedText;
const methodName = node.expression.name?.escapedText;
let objectName = node.expression.expression?.escapedText;

// Handle nested member expressions like window.DD_RUM.addAction
if (!objectName && ts.isPropertyAccessExpression(node.expression.expression)) {
// For window.DD_RUM.addAction, we want to check if it matches DD_RUM.addAction pattern
const nestedObjectName = node.expression.expression.name?.escapedText;
if (nestedObjectName) {
objectName = nestedObjectName;
}
}

if (!objectName || !methodName) {
return 'unknown';
Expand Down
8 changes: 4 additions & 4 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, 12);
assert.strictEqual(events.length, 15);

// Test Google Analytics event
const gaEvent = events.find(e => e.eventName === 'purchase' && e.source === 'googleanalytics');
Expand Down Expand Up @@ -176,7 +176,7 @@ test.describe('analyzeJsFile', () => {
assert.ok(snowplowEvent);
assert.strictEqual(snowplowEvent.source, 'snowplow');
assert.strictEqual(snowplowEvent.functionName, 'trackSnowplow');
assert.strictEqual(snowplowEvent.line, 138);
assert.strictEqual(snowplowEvent.line, 157);
assert.deepStrictEqual(snowplowEvent.properties, {
category: { type: 'string' },
label: { type: 'string' },
Expand All @@ -189,7 +189,7 @@ test.describe('analyzeJsFile', () => {
assert.ok(customEvent);
assert.strictEqual(customEvent.source, 'custom');
assert.strictEqual(customEvent.functionName, 'global');
assert.strictEqual(customEvent.line, 152);
assert.strictEqual(customEvent.line, 171);
assert.deepStrictEqual(customEvent.properties, {
userId: { type: 'string' },
order_id: { type: 'string' },
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, 11);
assert.strictEqual(events.length, 14);
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
});

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

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

// Test Google Analytics event
const gaEvent = events.find(e => e.eventName === 'order_completed' && e.source === 'googleanalytics');
assert.ok(gaEvent);
assert.strictEqual(gaEvent.source, 'googleanalytics');
assert.strictEqual(gaEvent.functionName, 'trackOrderCompletedGA');
assert.strictEqual(gaEvent.line, 105);
assert.strictEqual(gaEvent.line, 119);
assert.deepStrictEqual(gaEvent.properties, {
order_id: { type: 'string' },
products: {
Expand Down Expand Up @@ -76,7 +76,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(segmentEvent);
assert.strictEqual(segmentEvent.source, 'segment');
assert.strictEqual(segmentEvent.functionName, 'checkout');
assert.strictEqual(segmentEvent.line, 121);
assert.strictEqual(segmentEvent.line, 135);
assert.deepStrictEqual(segmentEvent.properties, {
stage: { type: 'string' },
method: { type: 'string' },
Expand All @@ -88,7 +88,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(mixpanelEvent);
assert.strictEqual(mixpanelEvent.source, 'mixpanel');
assert.strictEqual(mixpanelEvent.functionName, 'confirmPurchaseMixpanel');
assert.strictEqual(mixpanelEvent.line, 130);
assert.strictEqual(mixpanelEvent.line, 144);
assert.deepStrictEqual(mixpanelEvent.properties, {
order_id: { type: 'string' },
items: {
Expand All @@ -111,7 +111,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(amplitudeEvent);
assert.strictEqual(amplitudeEvent.source, 'amplitude');
assert.strictEqual(amplitudeEvent.functionName, 'checkout');
assert.strictEqual(amplitudeEvent.line, 135);
assert.strictEqual(amplitudeEvent.line, 149);
assert.deepStrictEqual(amplitudeEvent.properties, {
order_id: { type: 'string' },
items: {
Expand Down Expand Up @@ -143,7 +143,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(rudderstackEvent);
assert.strictEqual(rudderstackEvent.source, 'rudderstack');
assert.strictEqual(rudderstackEvent.functionName, 'checkout');
assert.strictEqual(rudderstackEvent.line, 150);
assert.strictEqual(rudderstackEvent.line, 164);
assert.deepStrictEqual(rudderstackEvent.properties, {
order_id: { type: 'string' },
items: {
Expand Down Expand Up @@ -174,7 +174,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(mparticleEvent);
assert.strictEqual(mparticleEvent.source, 'mparticle');
assert.strictEqual(mparticleEvent.functionName, 'checkout2');
assert.strictEqual(mparticleEvent.line, 176);
assert.strictEqual(mparticleEvent.line, 190);
assert.deepStrictEqual(mparticleEvent.properties, {
order_id: { type: 'string' },
items: {
Expand Down Expand Up @@ -205,7 +205,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(posthogEvent);
assert.strictEqual(posthogEvent.source, 'posthog');
assert.strictEqual(posthogEvent.functionName, 'checkout2');
assert.strictEqual(posthogEvent.line, 195);
assert.strictEqual(posthogEvent.line, 209);
assert.deepStrictEqual(posthogEvent.properties, {
order_id: { type: 'string' },
retry: { type: 'number' },
Expand Down Expand Up @@ -237,7 +237,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(pendoEvent);
assert.strictEqual(pendoEvent.source, 'pendo');
assert.strictEqual(pendoEvent.functionName, 'checkout3');
assert.strictEqual(pendoEvent.line, 216);
assert.strictEqual(pendoEvent.line, 230);
assert.deepStrictEqual(pendoEvent.properties, {
order_id: { type: 'string' },
products: {
Expand Down Expand Up @@ -268,7 +268,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(heapEvent);
assert.strictEqual(heapEvent.source, 'heap');
assert.strictEqual(heapEvent.functionName, 'checkout3');
assert.strictEqual(heapEvent.line, 230);
assert.strictEqual(heapEvent.line, 244);
assert.deepStrictEqual(heapEvent.properties, {
user_id: { type: 'string' },
email: { type: 'string' },
Expand All @@ -284,7 +284,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(snowplowEvent1);
assert.strictEqual(snowplowEvent1.source, 'snowplow');
assert.strictEqual(snowplowEvent1.functionName, 'trackSnowplow');
assert.strictEqual(snowplowEvent1.line, 247);
assert.strictEqual(snowplowEvent1.line, 282);
assert.deepStrictEqual(snowplowEvent1.properties, {
category: { type: 'string' },
label: { type: 'string' },
Expand All @@ -296,14 +296,14 @@ test.describe('analyzeTsFile', () => {
assert.ok(snowplowEvent2);
assert.strictEqual(snowplowEvent2.source, 'snowplow');
assert.strictEqual(snowplowEvent2.functionName, 'trackSnowplow2');
assert.strictEqual(snowplowEvent2.line, 251);
assert.strictEqual(snowplowEvent2.line, 286);

// Test custom function event
const customEvent = events.find(e => e.eventName === 'custom_event_v2');
assert.ok(customEvent);
assert.strictEqual(customEvent.source, 'custom');
assert.strictEqual(customEvent.functionName, 'global');
assert.strictEqual(customEvent.line, 280);
assert.strictEqual(customEvent.line, 315);
assert.deepStrictEqual(customEvent.properties, {
userId: { type: 'string' },
order_id: { type: 'string' },
Expand All @@ -326,7 +326,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(constantEvent);
assert.strictEqual(constantEvent.source, 'custom');
assert.strictEqual(constantEvent.functionName, 'global');
assert.strictEqual(constantEvent.line, 291);
assert.strictEqual(constantEvent.line, 326);
assert.deepStrictEqual(constantEvent.properties, {
userId: { type: 'string' },
orderId: { type: 'string' },
Expand All @@ -342,7 +342,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(importedConstantEvent);
assert.strictEqual(importedConstantEvent.source, 'segment');
assert.strictEqual(importedConstantEvent.functionName, 'global');
assert.strictEqual(importedConstantEvent.line, 293);
assert.strictEqual(importedConstantEvent.line, 328);
assert.deepStrictEqual(importedConstantEvent.properties, {
orderId: { type: 'string' },
total: { type: 'number' },
Expand All @@ -357,7 +357,7 @@ test.describe('analyzeTsFile', () => {
assert.ok(frozenConstantEvent);
assert.strictEqual(frozenConstantEvent.source, 'mixpanel');
assert.strictEqual(frozenConstantEvent.functionName, 'global');
assert.strictEqual(frozenConstantEvent.line, 292);
assert.strictEqual(frozenConstantEvent.line, 327);
assert.deepStrictEqual(frozenConstantEvent.properties, {
orderId: { type: 'string' },
total: { type: 'number' },
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, 13);
assert.strictEqual(events.length, 16);
assert.strictEqual(events.find(e => e.source === 'custom'), undefined);
});

Expand Down
19 changes: 19 additions & 0 deletions tests/fixtures/javascript/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,25 @@ export const checkout3 = function() {
email: '[email protected]',
name: 'John Doe'
});

// datadog tracking examples - all three patterns
datadogRum.addAction('checkout', {
total: 500,
order_id: 'ABC123',
currency: 'USD'
});

window.DD_RUM.addAction('user_login', {
user_id: 'user123',
method: 'email',
success: true
});

DD_RUM.addAction('page_view', {
page: '/checkout',
section: 'payment',
user_type: 'premium'
});
}

class MyClass {
Expand Down
Loading