Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type Dependencies } from '../../iocContainer/index.js';
2import { inject } from '../../iocContainer/utils.js';
3import {
4 type ItemSubmission,
5 type NormalizedItemData,
6 type RawItemData,
7} from '../../services/itemProcessingService/index.js';
8import { fromCorrelationId } from '../../utils/correlationIds.js';
9import { jsonStringifyUnstable } from '../../utils/encoding.js';
10import { getUtcDateOnlyString } from '../../utils/time.js';
11import { type RuleExecutionCorrelationId } from './ruleExecutionLoggingUtils.js';
12
13// NB: when an incoming POST /content api request fails, the content submission
14// logged to the data warehouse might not be in a valid, processable shape (in fact, it
15// may be that the content api request failed _because_ the content submission
16// was invalid).
17export type ContentApiRequestLogEntry<HasFailure extends boolean> = {
18 requestId: RuleExecutionCorrelationId;
19 orgId: string;
20 itemSubmission: Pick<
21 ItemSubmission,
22 'submissionId' | 'creator' | 'itemId' | 'itemType' | 'submissionTime'
23 > &
24 (HasFailure extends false
25 ? { data: NormalizedItemData }
26 : { data: NormalizedItemData | RawItemData });
27 failureReason: HasFailure extends true
28 ? string
29 : HasFailure extends false
30 ? undefined
31 : string | undefined;
32};
33
34export type ContentDetailsApiRequestLogEntry = {
35 orgId: string;
36 contentId: string;
37 failureReason?: string;
38};
39
40class ContentApiLogger {
41 constructor(
42 private readonly analytics: Dependencies['DataWarehouseAnalytics'],
43 private readonly dataWarehouse: Dependencies['DataWarehouse'],
44 private readonly tracer: Dependencies['Tracer'],
45 ) {}
46
47 async logContentApiRequest<HasFailure extends boolean>(
48 data: ContentApiRequestLogEntry<HasFailure>,
49 skipBatch: boolean,
50 ) {
51 const { failureReason, itemSubmission } = data;
52 const { itemType } = itemSubmission;
53 const now = new Date();
54 await this.analytics.bulkWrite(
55 'CONTENT_API_REQUESTS' as any,
56 [
57 {
58 ds: getUtcDateOnlyString(now),
59 ts: now.valueOf(),
60 item_id: itemSubmission.itemId,
61 item_data: jsonStringifyUnstable(itemSubmission.data),
62 ...(itemSubmission.creator !== undefined
63 ? {
64 item_creator_id: itemSubmission.creator.id,
65 item_creator_type_id: itemSubmission.creator.typeId,
66 }
67 : {}),
68 item_type_kind: itemType.kind,
69 item_type_name: itemType.name,
70 item_type_version: itemType.version,
71 item_type_schema_variant: itemType.schemaVariant,
72 item_type_id: itemType.id,
73 item_type_schema: jsonStringifyUnstable(itemType.schema),
74 item_type_schema_field_roles: itemType.schemaFieldRoles,
75 org_id: data.orgId,
76 request_id: fromCorrelationId(data.requestId),
77 submission_id: itemSubmission.submissionId,
78
79 ...(failureReason != null
80 ? {
81 event: 'REQUEST_FAILED' as const,
82 failure_reason: failureReason,
83 }
84 : { event: 'REQUEST_SUCCEEDED' as const }),
85 },
86 ],
87 { batchTimeout: skipBatch ? 0 : undefined },
88 );
89 }
90
91 async logContentDetailsApiRequest(data: ContentDetailsApiRequestLogEntry) {
92 const { failureReason } = data;
93 const now = new Date();
94 await this.dataWarehouse.query(
95 `INSERT INTO CONTENT_DETAILS_API_REQUESTS
96 (ds, ts, content_id, org_id, event${
97 failureReason ? ', failure_reason' : ''
98 })
99 VALUES (?, ?, ?, ?, ?${failureReason ? `, ?` : ''});`,
100 this.tracer,
101 [
102 getUtcDateOnlyString(now),
103 now.valueOf(),
104 data.contentId,
105 data.orgId,
106 ...(failureReason != null
107 ? ['REQUEST_FAILED', failureReason]
108 : ['REQUEST_SUCCEEDED']),
109 ],
110 );
111 }
112}
113
114export default inject(
115 ['DataWarehouseAnalytics', 'DataWarehouse', 'Tracer'],
116 ContentApiLogger,
117);
118export { type ContentApiLogger };