@cocreate/usage 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/index.js +251 -207
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.3.0...v1.4.0) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enhance event listener handling and improve logging for organization deletion ([cca2e40](https://github.com/CoCreate-app/CoCreate-usage/commit/cca2e408ba2c6599aab2b56430b1d74b923fbb57))
|
|
7
|
+
* refactor usage tracking with new bandwidth management and event listeners ([76f8ddb](https://github.com/CoCreate-app/CoCreate-usage/commit/76f8ddb539ad6a3384ed5d1557b1e36b9d64222a))
|
|
8
|
+
|
|
1
9
|
# [1.3.0](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.2.2...v1.3.0) (2026-07-17)
|
|
2
10
|
|
|
3
11
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -13,232 +13,276 @@
|
|
|
13
13
|
*
|
|
14
14
|
* You should have received a copy of the GNU Affero General Public License
|
|
15
15
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*
|
|
17
16
|
********************************************************************************/
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
// For commercial use of this software without the copyleft provisions of the AGPLv3,
|
|
21
|
-
// you must obtain a commercial license from CoCreate LLC.
|
|
22
|
-
// For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
|
|
23
|
-
|
|
24
|
-
import server from '@cocreate/server';
|
|
25
|
-
|
|
26
|
-
// ==========================================
|
|
27
|
-
// Module-Level Sandbox State (ESM Singleton Container)
|
|
28
|
-
// ==========================================
|
|
29
|
-
const organizations = new Map();
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Normalizes input data types and calculates payload sizing in bytes.
|
|
33
|
-
*/
|
|
34
|
-
function getBytes(data) {
|
|
35
|
-
if (typeof data === 'number')
|
|
36
|
-
return data;
|
|
37
|
-
else if (data instanceof Buffer)
|
|
38
|
-
return data.byteLength || 0;
|
|
39
|
-
else if (data instanceof String || typeof data === 'string')
|
|
40
|
-
return Buffer.byteLength(data, 'utf8') || 0;
|
|
41
|
-
else if (typeof data === 'object' && data !== null)
|
|
42
|
-
return Buffer.byteLength(JSON.stringify(data), 'utf8') || 0;
|
|
43
|
-
else return 0;
|
|
44
|
-
}
|
|
18
|
+
import crud from '@cocreate/crud-server';
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function getRate(totalUsage = 0) {
|
|
50
|
-
const tiers = [
|
|
51
|
-
{ limit: 1, rate: 10 },
|
|
52
|
-
{ limit: 10, rate: 5 },
|
|
53
|
-
{ limit: 100, rate: 2.5 },
|
|
54
|
-
{ limit: 1000, rate: 1.125 },
|
|
55
|
-
{ limit: 10000, rate: 0.562 },
|
|
56
|
-
{ limit: 100000, rate: 0.281 },
|
|
57
|
-
{ limit: 1000000, rate: 0.145 }
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const matchingTier = tiers.find(tier => totalUsage < tier.limit);
|
|
61
|
-
return matchingTier ? matchingTier.rate : 0.12;
|
|
62
|
-
}
|
|
20
|
+
class CoCreateUsage {
|
|
21
|
+
constructor(platformOrgId) {
|
|
22
|
+
this.platformOrgId = platformOrgId; // Master platform organization ID
|
|
63
23
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const platformOrganization = server.organization_id;
|
|
75
|
-
let organization = await server.crud.send({
|
|
76
|
-
method: 'object.read',
|
|
77
|
-
// host: server.host,
|
|
78
|
-
array: 'organizations',
|
|
79
|
-
object: { _id: organization_id },
|
|
80
|
-
organization_id: platformOrganization
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
org.dataTransferedIn += getBytes(platformOrganization);
|
|
84
|
-
org.dataTransferedInCount++;
|
|
85
|
-
|
|
86
|
-
if (organization && organization.object && organization.object[0]) {
|
|
87
|
-
organization = organization.object[0];
|
|
88
|
-
} else return;
|
|
89
|
-
|
|
90
|
-
if (server.wsManager && server.wsManager.organizations.has(organization_id)) {
|
|
91
|
-
const socketOrg = server.wsManager.organizations.get(organization_id);
|
|
92
|
-
if (organization.balance <= 0) {
|
|
93
|
-
socketOrg.status = false;
|
|
94
|
-
socketOrg.organizationBalance = false;
|
|
95
|
-
socketOrg.error = 'Your balance has fallen below 0';
|
|
96
|
-
} else {
|
|
97
|
-
socketOrg.status = true;
|
|
98
|
-
socketOrg.organizationBalance = true;
|
|
99
|
-
socketOrg.error = '';
|
|
100
|
-
}
|
|
24
|
+
// Constants
|
|
25
|
+
this.FLUSH_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 Megabytes
|
|
26
|
+
this.TENANT_INTERVAL_MS = 60 * 1000; // 1 Minute
|
|
27
|
+
|
|
28
|
+
// Double-buffer map tracking separate platform and tenant blocks per organization
|
|
29
|
+
this.bandwidthMap = new Map();
|
|
30
|
+
|
|
31
|
+
// Initialize the Event Listeners
|
|
32
|
+
this.initEventListeners();
|
|
101
33
|
}
|
|
102
34
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Connects directly to process event signals.
|
|
37
|
+
*/
|
|
38
|
+
initEventListeners() {
|
|
39
|
+
// Unified Application-Wide System Interceptor
|
|
40
|
+
// Expects an input object: { organization_id, data, type: 'ingress' | 'egress' }
|
|
41
|
+
process.on('usage', (payload) => {
|
|
42
|
+
if (!payload || typeof payload !== 'object') return;
|
|
43
|
+
|
|
44
|
+
const { organization_id, data, type } = payload;
|
|
45
|
+
if (organization_id) {
|
|
46
|
+
this.trackUsage(organization_id, data, type || 'ingress');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Dynamic Eviction Loop: Caught via native process messaging.
|
|
51
|
+
// Triggers clean flushes and timer de-allocations during standard disconnects or orchestrator shutdowns.
|
|
52
|
+
process.on('orgDeleted', async (organization_id) => {
|
|
53
|
+
if (this.bandwidthMap.has(organization_id)) {
|
|
54
|
+
console.log(`[USAGE] Event Received: Flushing pending data pipelines for Org: ${organization_id}`);
|
|
55
|
+
try {
|
|
56
|
+
await this.flushAndClearSingle(organization_id);
|
|
57
|
+
console.log(`[USAGE] Flushing Completed: Memory registry successfully cleared for Org: ${organization_id}`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(`[USAGE ERROR] Failed to cleanly flush data during orgDeleted event for ${organization_id}:`, err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
108
63
|
}
|
|
109
64
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
65
|
+
/**
|
|
66
|
+
* High-performance, robust byte-calculation routine.
|
|
67
|
+
* Accurately parses binary files (images, video chunks), text responses, and data objects.
|
|
68
|
+
*/
|
|
69
|
+
calculatePayloadBytes(data) {
|
|
70
|
+
if (data === null || data === undefined) return 0;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Case 1: Raw media streams, files, or binary chunks (Buffers / TypedArrays)
|
|
74
|
+
if (Buffer.isBuffer(data)) {
|
|
75
|
+
return data.length;
|
|
76
|
+
}
|
|
77
|
+
if (data instanceof Uint8Array || data.buffer instanceof ArrayBuffer) {
|
|
78
|
+
return data.byteLength || data.length || 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Case 2: Plain Text / Base64 Encoded Strings
|
|
82
|
+
if (typeof data === 'string') {
|
|
83
|
+
return Buffer.byteLength(data, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Case 3: Native JavaScript Objects & Arrays (Database schemas, API JSON packets)
|
|
87
|
+
if (typeof data === 'object') {
|
|
88
|
+
return Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Case 4: Fallback for primitive datatypes (numbers, booleans, etc.)
|
|
92
|
+
return Buffer.byteLength(String(data), 'utf8');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[USAGE ERROR] Failed to safely calculate payload byte scale:', error);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Main entry point to track incoming traffic.
|
|
101
|
+
*/
|
|
102
|
+
trackUsage(organization_id, data, direction = 'ingress') {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const incomingBytes = this.calculatePayloadBytes(data);
|
|
105
|
+
|
|
106
|
+
// If this is a brand new organization session, initialize our distinct tracking layers
|
|
107
|
+
if (!this.bandwidthMap.has(organization_id)) {
|
|
108
|
+
const tenantTimer = setInterval(() => {
|
|
109
|
+
this.flushTenantWindow(organization_id);
|
|
110
|
+
}, this.TENANT_INTERVAL_MS);
|
|
111
|
+
|
|
112
|
+
this.bandwidthMap.set(organization_id, {
|
|
113
|
+
// Buffer A: Tenant 1-minute accumulator (system_usage)
|
|
114
|
+
tenant: {
|
|
115
|
+
ingressBytes: 0,
|
|
116
|
+
egressBytes: 0,
|
|
117
|
+
totalBytes: 0,
|
|
118
|
+
count: 0,
|
|
119
|
+
sampleStart: now,
|
|
120
|
+
sampleEnd: now
|
|
121
|
+
},
|
|
122
|
+
// Buffer B: Platform master accumulator (system_bandwidth)
|
|
123
|
+
platform: {
|
|
124
|
+
ingressBytes: 0,
|
|
125
|
+
egressBytes: 0,
|
|
126
|
+
totalBytes: 0,
|
|
127
|
+
count: 0,
|
|
128
|
+
sampleStart: now,
|
|
129
|
+
sampleEnd: now
|
|
124
130
|
},
|
|
125
|
-
|
|
126
|
-
timeStamp
|
|
131
|
+
tenantTimer
|
|
127
132
|
});
|
|
128
|
-
organization.dataTransfered = 0;
|
|
129
133
|
}
|
|
130
|
-
}
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
dataTransfered = dataTransfered / 1073741824;
|
|
134
|
-
dataTransfered = dataTransfered.toFixed(32);
|
|
135
|
-
dataTransfered = parseFloat(dataTransfered);
|
|
136
|
-
|
|
137
|
-
let rate = getRate(organization.dataTransfered);
|
|
138
|
-
let amount = dataTransfered * rate;
|
|
139
|
-
amount = -amount;
|
|
140
|
-
amount = amount.toFixed(32);
|
|
141
|
-
amount = parseFloat(amount);
|
|
142
|
-
|
|
143
|
-
let balanceUpdate = {
|
|
144
|
-
method: 'object.update',
|
|
145
|
-
// host: server.host,
|
|
146
|
-
array: 'organizations',
|
|
147
|
-
object: { _id: organization_id },
|
|
148
|
-
organization_id: platformOrganization,
|
|
149
|
-
timeStamp
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
if (isExpired)
|
|
153
|
-
balanceUpdate.object['balance'] = 0;
|
|
154
|
-
else
|
|
155
|
-
balanceUpdate.object.$inc = { balance: amount };
|
|
156
|
-
|
|
157
|
-
if (isResetDataTransfer)
|
|
158
|
-
balanceUpdate.object['dataTransfered'] = 0;
|
|
159
|
-
else if (!balanceUpdate.object.$inc)
|
|
160
|
-
balanceUpdate.object.$inc = { dataTransfered };
|
|
161
|
-
else
|
|
162
|
-
balanceUpdate.object.$inc.dataTransfered = dataTransfered;
|
|
163
|
-
|
|
164
|
-
server.crud.send(balanceUpdate);
|
|
165
|
-
|
|
166
|
-
let transaction = {
|
|
167
|
-
method: 'object.create',
|
|
168
|
-
host,
|
|
169
|
-
array: 'transactions',
|
|
170
|
-
object: {
|
|
171
|
-
organization_id,
|
|
172
|
-
type: "withdrawal",
|
|
173
|
-
amount,
|
|
174
|
-
rate,
|
|
175
|
-
dataTransfered,
|
|
176
|
-
...org
|
|
177
|
-
},
|
|
178
|
-
organization_id,
|
|
179
|
-
timeStamp
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
server.crud.send(transaction);
|
|
183
|
-
}
|
|
135
|
+
const stats = this.bandwidthMap.get(organization_id);
|
|
184
136
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
organizations.delete(organization_id);
|
|
201
|
-
}, 60000);
|
|
202
|
-
|
|
203
|
-
org.dataTransferedIn = 0;
|
|
204
|
-
org.dataTransferedInCount = 0;
|
|
205
|
-
org.dataTransferedOut = 0;
|
|
206
|
-
org.dataTransferedOutCount = 0;
|
|
207
|
-
|
|
208
|
-
organizations.set(organization_id, org);
|
|
137
|
+
// 1. Accumulate Tenant Buffer (Flushes on interval)
|
|
138
|
+
if (direction === 'ingress') {
|
|
139
|
+
stats.tenant.ingressBytes += incomingBytes;
|
|
140
|
+
} else if (direction === 'egress') {
|
|
141
|
+
stats.tenant.egressBytes += incomingBytes;
|
|
142
|
+
}
|
|
143
|
+
stats.tenant.totalBytes += incomingBytes;
|
|
144
|
+
stats.tenant.count += 1;
|
|
145
|
+
stats.tenant.sampleEnd = now;
|
|
146
|
+
|
|
147
|
+
// 2. Accumulate Platform Buffer (Flushes on event triggers / 10MB only)
|
|
148
|
+
if (direction === 'ingress') {
|
|
149
|
+
stats.platform.ingressBytes += incomingBytes;
|
|
150
|
+
} else if (direction === 'egress') {
|
|
151
|
+
stats.platform.egressBytes += incomingBytes;
|
|
209
152
|
}
|
|
153
|
+
stats.platform.totalBytes += incomingBytes;
|
|
154
|
+
stats.platform.count += 1;
|
|
155
|
+
stats.platform.sampleEnd = now;
|
|
210
156
|
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} else if (type === 'out') {
|
|
215
|
-
org.dataTransferedOut = dataTransfered;
|
|
216
|
-
org.dataTransferedOutCount++;
|
|
217
|
-
} else {
|
|
218
|
-
console.log('else');
|
|
157
|
+
// 3. Platform Trigger: Flush to the master platform ledger if we hit 10MB
|
|
158
|
+
if (stats.platform.totalBytes >= this.FLUSH_THRESHOLD_BYTES) {
|
|
159
|
+
this.flushPlatformWindow(organization_id);
|
|
219
160
|
}
|
|
161
|
+
}
|
|
220
162
|
|
|
221
|
-
|
|
222
|
-
|
|
163
|
+
/**
|
|
164
|
+
* PIPELINE A: Handles the 1-minute interval flush to the client tenant's database
|
|
165
|
+
*/
|
|
166
|
+
async flushTenantWindow(organization_id) {
|
|
167
|
+
const stats = this.bandwidthMap.get(organization_id);
|
|
168
|
+
if (!stats || stats.tenant.totalBytes === 0) return;
|
|
169
|
+
|
|
170
|
+
const tenantData = stats.tenant;
|
|
171
|
+
|
|
172
|
+
// Save exactly one document representing this complete 1-minute segment to their partition
|
|
173
|
+
await this.saveToTenantBilling(organization_id, tenantData);
|
|
174
|
+
|
|
175
|
+
// Reset ONLY the tenant accumulator to begin a fresh 1-minute block
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
stats.tenant = {
|
|
178
|
+
ingressBytes: 0,
|
|
179
|
+
egressBytes: 0,
|
|
180
|
+
totalBytes: 0,
|
|
181
|
+
count: 0,
|
|
182
|
+
sampleStart: now,
|
|
183
|
+
sampleEnd: now
|
|
184
|
+
};
|
|
223
185
|
}
|
|
224
|
-
}
|
|
225
186
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
187
|
+
/**
|
|
188
|
+
* PIPELINE B: Handles targeted platform flushes (when 10MB limit is reached)
|
|
189
|
+
*/
|
|
190
|
+
async flushPlatformWindow(organization_id) {
|
|
191
|
+
const stats = this.bandwidthMap.get(organization_id);
|
|
192
|
+
if (!stats || stats.platform.totalBytes === 0) return;
|
|
193
|
+
|
|
194
|
+
const platformData = stats.platform;
|
|
195
|
+
|
|
196
|
+
// Save platform ledger record
|
|
197
|
+
await this.saveToLedgerDatabase(organization_id, platformData);
|
|
198
|
+
|
|
199
|
+
// Reset ONLY the platform long-term memory accumulator
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
stats.platform = {
|
|
202
|
+
ingressBytes: 0,
|
|
203
|
+
egressBytes: 0,
|
|
204
|
+
totalBytes: 0,
|
|
205
|
+
count: 0,
|
|
206
|
+
sampleStart: now,
|
|
207
|
+
sampleEnd: now
|
|
208
|
+
};
|
|
238
209
|
}
|
|
239
|
-
}
|
|
240
210
|
|
|
241
|
-
|
|
242
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Target eviction: Clears timers and flushes all outstanding tenant & platform balances
|
|
213
|
+
*/
|
|
214
|
+
async flushAndClearSingle(organization_id) {
|
|
215
|
+
const stats = this.bandwidthMap.get(organization_id);
|
|
216
|
+
if (!stats) return;
|
|
217
|
+
|
|
218
|
+
// De-allocate the 1-minute background loop instantly
|
|
219
|
+
if (stats.tenantTimer) {
|
|
220
|
+
clearInterval(stats.tenantTimer);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Flush any partial unsaved records remaining in either pipeline
|
|
224
|
+
await Promise.all([
|
|
225
|
+
this.flushTenantWindow(organization_id),
|
|
226
|
+
this.flushPlatformWindow(organization_id)
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
// Evict from active tracker
|
|
230
|
+
this.bandwidthMap.delete(organization_id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* CRUD Adapter: Platform Bandwidth Collection (Private time-series log)
|
|
235
|
+
* Writes into the platform's sandbox segment, cataloging target clients in "organization".
|
|
236
|
+
*/
|
|
237
|
+
async saveToLedgerDatabase(organization_id, stats) {
|
|
238
|
+
try {
|
|
239
|
+
await crud.send({
|
|
240
|
+
method: 'object.create',
|
|
241
|
+
array: 'system_bandwidth',
|
|
242
|
+
object: {
|
|
243
|
+
organization_id: this.platformOrgId, // Platform workspace sandbox route
|
|
244
|
+
organization: organization_id, // The actual client tenant who generated the bandwidth
|
|
245
|
+
ingressBytes: stats.ingressBytes,
|
|
246
|
+
egressBytes: stats.egressBytes,
|
|
247
|
+
totalBytes: stats.totalBytes,
|
|
248
|
+
totalRequests: stats.count,
|
|
249
|
+
sampleStart: new Date(stats.sampleStart),
|
|
250
|
+
sampleEnd: new Date(stats.sampleEnd),
|
|
251
|
+
durationMs: stats.sampleEnd - stats.sampleStart,
|
|
252
|
+
processed: false // Ready for asynchronous billing calculations
|
|
253
|
+
},
|
|
254
|
+
organization_id: this.platformOrgId // Outermost routing targeting the Platform's DB partition
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(`[DB ERROR] Failed to write to system_bandwidth for ${organization_id}:`, error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* CRUD Adapter: Tenant Usage Collection (Their 1-Minute Dashboard Stream)
|
|
263
|
+
* Writes directly to the tenant's partition under the safe, system namespace "system_usage"
|
|
264
|
+
*/
|
|
265
|
+
async saveToTenantBilling(organization_id, stats) {
|
|
266
|
+
try {
|
|
267
|
+
await crud.send({
|
|
268
|
+
method: 'object.create',
|
|
269
|
+
array: 'system_usage',
|
|
270
|
+
object: {
|
|
271
|
+
organization_id, // Tenant's sandbox segment route
|
|
272
|
+
ingressBytes: stats.ingressBytes,
|
|
273
|
+
egressBytes: stats.egressBytes,
|
|
274
|
+
totalBytes: stats.totalBytes,
|
|
275
|
+
totalRequests: stats.count,
|
|
276
|
+
sampleStart: new Date(stats.sampleStart),
|
|
277
|
+
sampleEnd: new Date(stats.sampleEnd),
|
|
278
|
+
durationMs: stats.sampleEnd - stats.sampleStart
|
|
279
|
+
},
|
|
280
|
+
organization_id: organization_id // Outermost routing targeting the Client's DB partition
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error(`[DB ERROR] Failed to write to system_usage for ${organization_id}:`, error);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
243
287
|
|
|
244
|
-
export default
|
|
288
|
+
export default CoCreateUsage;
|