Skip to content

Commit d1ff4e6

Browse files
committed
feat: add application batch endpoint
1 parent acf452b commit d1ff4e6

File tree

2 files changed

+126
-42
lines changed

2 files changed

+126
-42
lines changed

src/server/model/application/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@ export interface ApplicationEventPayload {
1919
url?: string;
2020
application: string;
2121
name?: string;
22+
version?: string;
23+
sdkVersion?: string;
2224
}
2325

24-
export async function findSession(req: Request): Promise<
26+
export async function findSession(
27+
req: Request,
28+
body: any
29+
): Promise<
2530
ApplicationSession & {
2631
workspaceId: string;
2732
}
2833
> {
2934
// Verify payload
30-
const { payload } = req.body;
35+
const { payload } = body;
3136

3237
// Check if cache token is passed
3338
const cacheToken = req.headers['x-tianji-cache'] as string;
@@ -44,6 +49,8 @@ export async function findSession(req: Request): Promise<
4449
application: applicationId,
4550
os,
4651
language,
52+
version,
53+
sdkVersion,
4754
} = payload as ApplicationEventPayload;
4855

4956
if (!isCuid(applicationId)) {
@@ -84,6 +91,8 @@ export async function findSession(req: Request): Promise<
8491
os,
8592
language,
8693
ip,
94+
version,
95+
sdkVersion,
8796
country,
8897
subdivision1,
8998
subdivision2,

src/server/router/application.ts

Lines changed: 115 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,54 @@ import { isWorkspacePaused } from '../model/billing/workspace.js';
1212

1313
export const applicationRouter = Router();
1414

15+
// Shared validation schema for event payload
16+
const eventPayloadSchema = yup.object().shape({
17+
data: yup.object(),
18+
language: yup.string().max(35),
19+
os: yup.string().max(20),
20+
application: yup.string().required(),
21+
name: yup.string().max(50),
22+
screen: yup.string().max(500),
23+
params: yup.object(),
24+
});
25+
26+
// Shared function to process a single event
27+
async function processEvent(type: string, payload: any, session: any) {
28+
const {
29+
name: eventName,
30+
data: eventData,
31+
screen: screenName,
32+
params: screenParams,
33+
} = payload;
34+
35+
if (type === COLLECTION_TYPE.event) {
36+
await saveApplicationEvent({
37+
eventName,
38+
eventData,
39+
screenName,
40+
screenParams,
41+
...session,
42+
sessionId: session.id,
43+
});
44+
return { status: 'success', type: 'event' };
45+
}
46+
47+
if (type === COLLECTION_TYPE.identify) {
48+
if (!eventData) {
49+
throw new Error('Data required for identify event');
50+
}
51+
52+
await saveApplicationSessionData({
53+
...session,
54+
sessionData: eventData,
55+
sessionId: session.id,
56+
});
57+
return { status: 'success', type: 'identify' };
58+
}
59+
60+
throw new Error(`Unsupported event type: ${type}`);
61+
}
62+
1563
applicationRouter.post(
1664
'/send',
1765
validate(
@@ -20,20 +68,7 @@ applicationRouter.post(
2068
.withMessage('payload should be existed')
2169
.isObject()
2270
.custom(async (input) => {
23-
return yup
24-
.object()
25-
.shape({
26-
data: yup.object(),
27-
language: yup.string().max(35),
28-
os: yup.string().max(20),
29-
url: yup.string().max(500),
30-
application: yup.string().required(),
31-
name: yup.string().max(50),
32-
screen: yup.string().max(500),
33-
params: yup.object(),
34-
})
35-
.required()
36-
.validate(input);
71+
return eventPayloadSchema.required().validate(input);
3772
}),
3873
body('type')
3974
.exists()
@@ -43,46 +78,86 @@ applicationRouter.post(
4378
),
4479
async (req, res) => {
4580
const { type, payload } = req.body;
46-
const {
47-
url,
48-
name: eventName,
49-
data: eventData,
50-
screen: screenName,
51-
params: screenParams,
52-
} = payload;
5381

54-
const session = await findSession(req);
82+
const session = await findSession(req, req.body);
5583

5684
if (await isWorkspacePaused(session.workspaceId)) {
5785
res.status(403).send('Workspace is paused.');
5886
return;
5987
}
6088

61-
if (type === COLLECTION_TYPE.event) {
62-
await saveApplicationEvent({
63-
eventName,
64-
eventData,
65-
screenName,
66-
screenParams,
67-
...session,
68-
sessionId: session.id,
89+
try {
90+
await processEvent(type, payload, session);
91+
const token = createToken(session);
92+
res.send(token);
93+
} catch (error) {
94+
res.status(400).json({
95+
error: error instanceof Error ? error.message : 'Unknown error',
6996
});
7097
}
98+
}
99+
);
71100

72-
if (type === COLLECTION_TYPE.identify) {
73-
if (!eventData) {
74-
throw new Error('Data required');
75-
}
101+
applicationRouter.post(
102+
'/batch',
103+
validate(
104+
body('events')
105+
.exists()
106+
.withMessage('events should be existed')
107+
.isArray({ min: 1, max: 100 })
108+
.withMessage('events should be an array with 1-100 items')
109+
.custom(async (events) => {
110+
// Validate each event in the array
111+
const eventSchema = yup.object().shape({
112+
type: yup
113+
.string()
114+
.required()
115+
.matches(/event|identify/i),
116+
payload: eventPayloadSchema.required(),
117+
});
76118

77-
await saveApplicationSessionData({
78-
...session,
79-
sessionData: eventData,
80-
sessionId: session.id,
81-
});
119+
for (const event of events) {
120+
await eventSchema.validate(event);
121+
}
122+
return true;
123+
})
124+
),
125+
async (req, res) => {
126+
const { events } = req.body;
127+
128+
const session = await findSession(req, events[0]);
129+
130+
if (await isWorkspacePaused(session.workspaceId)) {
131+
res.status(403).send('Workspace is paused.');
132+
return;
133+
}
134+
135+
const results = [];
136+
const errors = [];
137+
138+
// Process each event in the batch
139+
for (let i = 0; i < events.length; i++) {
140+
const event = events[i];
141+
try {
142+
const result = await processEvent(event.type, event.payload, session);
143+
results.push({ index: i, ...result });
144+
} catch (error) {
145+
errors.push({
146+
index: i,
147+
status: 'error',
148+
message: error instanceof Error ? error.message : 'Unknown error',
149+
});
150+
}
82151
}
83152

84153
const token = createToken(session);
85154

86-
res.send(token);
155+
res.json({
156+
token,
157+
processed: results.length,
158+
errors: errors.length,
159+
results,
160+
errorDetails: errors.length > 0 ? errors : undefined,
161+
});
87162
}
88163
);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy