Skip to content

Commit 76adadd

Browse files
committed
fix: Include properties in export/events Payload
1 parent f8f470a commit 76adadd

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* Script to test the Export API endpoints
3+
*
4+
* Specifically tests that the /export/events endpoint includes event properties in the payload
5+
*
6+
* Usage:
7+
* pnpm jiti scripts/test-export-api.ts
8+
*
9+
* Environment variables:
10+
* CLIENT_ID: Export API client ID (with read or root permissions)
11+
* CLIENT_SECRET: Export API client secret
12+
* PROJECT_ID: Project ID to test against
13+
* API_URL: API base URL (default: http://localhost:3333)
14+
*/
15+
16+
const CLIENT_ID = process.env.CLIENT_ID!;
17+
const CLIENT_SECRET = process.env.CLIENT_SECRET!;
18+
const PROJECT_ID = process.env.PROJECT_ID!;
19+
const API_BASE_URL = process.env.API_URL || 'http://localhost:3333';
20+
21+
if (!CLIENT_ID || !CLIENT_SECRET || !PROJECT_ID) {
22+
console.error('CLIENT_ID, CLIENT_SECRET, and PROJECT_ID must be set');
23+
process.exit(1);
24+
}
25+
26+
interface TestResult {
27+
name: string;
28+
method: string;
29+
url: string;
30+
status: number;
31+
success: boolean;
32+
error?: string;
33+
data?: any;
34+
}
35+
36+
const results: TestResult[] = [];
37+
38+
async function makeRequest(
39+
method: string,
40+
path: string,
41+
params?: Record<string, any>,
42+
): Promise<TestResult> {
43+
let url = `${API_BASE_URL}${path}`;
44+
45+
if (params && method === 'GET') {
46+
const searchParams = new URLSearchParams();
47+
for (const [key, value] of Object.entries(params)) {
48+
if (Array.isArray(value)) {
49+
searchParams.append(key, JSON.stringify(value));
50+
} else if (value instanceof Object) {
51+
searchParams.append(key, JSON.stringify(value));
52+
} else if (value !== undefined && value !== null) {
53+
searchParams.append(key, String(value));
54+
}
55+
}
56+
url += '?' + searchParams.toString();
57+
}
58+
59+
const headers: Record<string, string> = {
60+
'openpanel-client-id': CLIENT_ID,
61+
'openpanel-client-secret': CLIENT_SECRET,
62+
};
63+
64+
try {
65+
const controller = new AbortController();
66+
const timeout = setTimeout(() => controller.abort(), 15_000);
67+
const response = await fetch(url, {
68+
method,
69+
headers,
70+
signal: controller.signal,
71+
});
72+
clearTimeout(timeout);
73+
74+
const data = await response.json().catch(() => ({}));
75+
76+
return {
77+
name: `${method} ${path}`,
78+
method,
79+
url,
80+
status: response.status,
81+
success: response.ok,
82+
error: response.ok ? undefined : data.message || 'Request failed',
83+
data: response.ok ? data : undefined,
84+
};
85+
} catch (error) {
86+
return {
87+
name: `${method} ${path}`,
88+
method,
89+
url,
90+
status: 0,
91+
success: false,
92+
error: error instanceof Error ? error.message : 'Unknown error',
93+
};
94+
}
95+
}
96+
97+
async function testExportEvents() {
98+
console.log('\n📊 Testing Export Events endpoint...\n');
99+
100+
// Test 1: Basic events export without includes
101+
console.log('Test 1: Basic events export (should include properties by default)');
102+
const basicResult = await makeRequest('GET', '/export/events', {
103+
projectId: PROJECT_ID,
104+
limit: 10,
105+
});
106+
results.push(basicResult);
107+
108+
if (basicResult.success) {
109+
console.log(`✅ GET /export/events: ${basicResult.status}`);
110+
111+
if (basicResult.data?.data?.length > 0) {
112+
const firstEvent = basicResult.data.data[0];
113+
console.log(` Total events returned: ${basicResult.data.data.length}`);
114+
115+
// Check for properties field
116+
if (firstEvent.properties !== undefined) {
117+
console.log(` ✅ Properties field present: ${JSON.stringify(firstEvent.properties)}`);
118+
} else {
119+
console.log(` ❌ Properties field MISSING in event`);
120+
console.log(` Event keys: ${Object.keys(firstEvent).join(', ')}`);
121+
throw new Error('Test 1 FAILED: Properties field is missing from export/events response');
122+
}
123+
124+
// Log redacted event structure (keys only, no sensitive data)
125+
console.log(` Event keys: ${Object.keys(firstEvent).join(', ')}`);
126+
console.log(` Properties keys: ${Object.keys(firstEvent.properties || {}).join(', ')}`);
127+
} else {
128+
console.log(` ⚠️ No events returned for this project`);
129+
}
130+
} else {
131+
console.log(`❌ GET /export/events: ${basicResult.status}`);
132+
if (basicResult.error) console.log(` Error: ${basicResult.error}`);
133+
}
134+
135+
// Test 2: Events export with specific event filter
136+
console.log('\n\nTest 2: Events export with event filter');
137+
const filteredResult = await makeRequest('GET', '/export/events', {
138+
projectId: PROJECT_ID,
139+
event: 'screen_view',
140+
limit: 5,
141+
});
142+
results.push(filteredResult);
143+
144+
if (filteredResult.success) {
145+
console.log(`✅ GET /export/events (filtered): ${filteredResult.status}`);
146+
147+
if (filteredResult.data?.data?.length > 0) {
148+
const firstEvent = filteredResult.data.data[0];
149+
console.log(` Events returned: ${filteredResult.data.data.length}`);
150+
151+
if (firstEvent.properties !== undefined) {
152+
console.log(` ✅ Properties field present`);
153+
} else {
154+
console.log(` ❌ Properties field MISSING`);
155+
throw new Error('Test 2 FAILED: Properties field is missing from filtered export/events response');
156+
}
157+
} else {
158+
console.log(` ⚠️ No matching events found`);
159+
}
160+
} else {
161+
console.log(`❌ GET /export/events (filtered): ${filteredResult.status}`);
162+
}
163+
164+
// Test 3: Events export with profile include
165+
console.log('\n\nTest 3: Events export with profile include');
166+
const withProfileResult = await makeRequest('GET', '/export/events', {
167+
projectId: PROJECT_ID,
168+
includes: ['profile'],
169+
limit: 5,
170+
});
171+
results.push(withProfileResult);
172+
173+
if (withProfileResult.success) {
174+
console.log(`✅ GET /export/events (with profile): ${withProfileResult.status}`);
175+
176+
if (withProfileResult.data?.data?.length > 0) {
177+
const firstEvent = withProfileResult.data.data[0];
178+
179+
if (firstEvent.properties !== undefined) {
180+
console.log(` ✅ Properties field present`);
181+
} else {
182+
console.log(` ❌ Properties field MISSING`);
183+
throw new Error('Test 3 FAILED: Properties field is missing from export/events response with profile include');
184+
}
185+
186+
if (firstEvent.profile !== undefined) {
187+
console.log(` ✅ Profile field present (included)`);
188+
} else {
189+
console.log(` ⚠️ Profile field not included (expected due to permissions)`);
190+
}
191+
}
192+
} else {
193+
console.log(`❌ GET /export/events (with profile): ${withProfileResult.status}`);
194+
}
195+
196+
// Test 4: Events export with date range
197+
console.log('\n\nTest 4: Events export with date range');
198+
const now = new Date();
199+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
200+
201+
const dateRangeResult = await makeRequest('GET', '/export/events', {
202+
projectId: PROJECT_ID,
203+
start: weekAgo.toISOString().split('T')[0],
204+
end: now.toISOString().split('T')[0],
205+
limit: 5,
206+
});
207+
results.push(dateRangeResult);
208+
209+
if (dateRangeResult.success) {
210+
console.log(`✅ GET /export/events (date range): ${dateRangeResult.status}`);
211+
212+
if (dateRangeResult.data?.data?.length > 0) {
213+
const firstEvent = dateRangeResult.data.data[0];
214+
215+
if (firstEvent.properties !== undefined) {
216+
console.log(` ✅ Properties field present`);
217+
} else {
218+
console.log(` ❌ Properties field MISSING`);
219+
throw new Error('Test 4 FAILED: Properties field is missing from export/events response with date range');
220+
}
221+
}
222+
} else {
223+
console.log(`❌ GET /export/events (date range): ${dateRangeResult.status}`);
224+
}
225+
}
226+
227+
async function main() {
228+
console.log(`🚀 Export API Test Suite`);
229+
console.log(`Using API_URL: ${API_BASE_URL}`);
230+
console.log(`Using PROJECT_ID: ${PROJECT_ID}`);
231+
232+
await testExportEvents();
233+
234+
// Summary
235+
console.log('\n\n📋 Test Summary');
236+
console.log('─'.repeat(50));
237+
238+
const passed = results.filter((r) => r.success).length;
239+
const failed = results.filter((r) => !r.success).length;
240+
241+
console.log(`Total Tests: ${results.length}`);
242+
console.log(`✅ Passed: ${passed}`);
243+
console.log(`❌ Failed: ${failed}`);
244+
245+
if (failed > 0) {
246+
console.log('\nFailed Tests:');
247+
results.filter((r) => !r.success).forEach((r) => {
248+
console.log(` - ${r.name}: ${r.error || r.status}`);
249+
});
250+
process.exit(1);
251+
}
252+
253+
console.log('\n✨ All tests passed!');
254+
}
255+
256+
main().catch((error) => {
257+
console.error('Test suite error:', error);
258+
process.exit(1);
259+
});

apps/api/src/controllers/export.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export async function events(
114114
take,
115115
profileId: query.data.profileId,
116116
select: {
117+
properties: true,
117118
profile: false,
118119
meta: false,
119120
...query.data.includes?.reduce(

0 commit comments

Comments
 (0)