Content-Length: 692183 | pFad | http://github.com/msgbyte/tianji/commit/d1ff4e6f7b78bf904e9e65d4bd1b1ba4c31057c1

2C feat: add application batch endpoint · msgbyte/tianji@d1ff4e6 · GitHub
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)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/msgbyte/tianji/commit/d1ff4e6f7b78bf904e9e65d4bd1b1ba4c31057c1

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy