@mastra/upstash 0.12.1 → 0.12.2
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +53 -0
- package/dist/_tsup-dts-rollup.d.cts +342 -40
- package/dist/_tsup-dts-rollup.d.ts +342 -40
- package/dist/index.cjs +1133 -612
- package/dist/index.js +1134 -613
- package/docker-compose.yaml +1 -1
- package/package.json +5 -5
- package/src/storage/domains/legacy-evals/index.ts +279 -0
- package/src/storage/domains/memory/index.ts +902 -0
- package/src/storage/domains/operations/index.ts +168 -0
- package/src/storage/domains/scores/index.ts +216 -0
- package/src/storage/domains/traces/index.ts +172 -0
- package/src/storage/domains/utils.ts +57 -0
- package/src/storage/domains/workflows/index.ts +243 -0
- package/src/storage/index.test.ts +13 -0
- package/src/storage/index.ts +143 -1416
- package/src/storage/upstash.test.ts +0 -1461
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
2
|
+
import { StoreOperations } from '@mastra/core/storage';
|
|
3
|
+
import type { TABLE_NAMES, StorageColumn } from '@mastra/core/storage';
|
|
4
|
+
import type { Redis } from '@upstash/redis';
|
|
5
|
+
import { getKey, processRecord } from '../utils';
|
|
6
|
+
|
|
7
|
+
export class StoreOperationsUpstash extends StoreOperations {
|
|
8
|
+
private client: Redis;
|
|
9
|
+
|
|
10
|
+
constructor({ client }: { client: Redis }) {
|
|
11
|
+
super();
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async createTable({
|
|
16
|
+
tableName: _tableName,
|
|
17
|
+
schema: _schema,
|
|
18
|
+
}: {
|
|
19
|
+
tableName: TABLE_NAMES;
|
|
20
|
+
schema: Record<string, StorageColumn>;
|
|
21
|
+
}): Promise<void> {
|
|
22
|
+
// For Redis/Upstash, tables are created implicitly when data is inserted
|
|
23
|
+
// This method is a no-op for Redis-based storage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async alterTable({
|
|
27
|
+
tableName: _tableName,
|
|
28
|
+
schema: _schema,
|
|
29
|
+
ifNotExists: _ifNotExists,
|
|
30
|
+
}: {
|
|
31
|
+
tableName: TABLE_NAMES;
|
|
32
|
+
schema: Record<string, StorageColumn>;
|
|
33
|
+
ifNotExists: string[];
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
// For Redis/Upstash, schema changes are handled implicitly
|
|
36
|
+
// This method is a no-op for Redis-based storage
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
40
|
+
const pattern = `${tableName}:*`;
|
|
41
|
+
try {
|
|
42
|
+
await this.scanAndDelete(pattern);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new MastraError(
|
|
45
|
+
{
|
|
46
|
+
id: 'STORAGE_UPSTASH_STORAGE_CLEAR_TABLE_FAILED',
|
|
47
|
+
domain: ErrorDomain.STORAGE,
|
|
48
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
49
|
+
details: {
|
|
50
|
+
tableName,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
error,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
59
|
+
return this.clearTable({ tableName });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
|
|
63
|
+
const { key, processedRecord } = processRecord(tableName, record);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await this.client.set(key, processedRecord);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new MastraError(
|
|
69
|
+
{
|
|
70
|
+
id: 'STORAGE_UPSTASH_STORAGE_INSERT_FAILED',
|
|
71
|
+
domain: ErrorDomain.STORAGE,
|
|
72
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
73
|
+
details: {
|
|
74
|
+
tableName,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
error,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async batchInsert(input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
|
|
83
|
+
const { tableName, records } = input;
|
|
84
|
+
if (!records.length) return;
|
|
85
|
+
|
|
86
|
+
const batchSize = 1000;
|
|
87
|
+
try {
|
|
88
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
89
|
+
const batch = records.slice(i, i + batchSize);
|
|
90
|
+
const pipeline = this.client.pipeline();
|
|
91
|
+
for (const record of batch) {
|
|
92
|
+
const { key, processedRecord } = processRecord(tableName, record);
|
|
93
|
+
pipeline.set(key, processedRecord);
|
|
94
|
+
}
|
|
95
|
+
await pipeline.exec();
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new MastraError(
|
|
99
|
+
{
|
|
100
|
+
id: 'STORAGE_UPSTASH_STORAGE_BATCH_INSERT_FAILED',
|
|
101
|
+
domain: ErrorDomain.STORAGE,
|
|
102
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
103
|
+
details: {
|
|
104
|
+
tableName,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
error,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
|
|
113
|
+
const key = getKey(tableName, keys);
|
|
114
|
+
try {
|
|
115
|
+
const data = await this.client.get<R>(key);
|
|
116
|
+
return data || null;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new MastraError(
|
|
119
|
+
{
|
|
120
|
+
id: 'STORAGE_UPSTASH_STORAGE_LOAD_FAILED',
|
|
121
|
+
domain: ErrorDomain.STORAGE,
|
|
122
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
123
|
+
details: {
|
|
124
|
+
tableName,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
error,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async hasColumn(_tableName: TABLE_NAMES, _column: string): Promise<boolean> {
|
|
133
|
+
// For Redis/Upstash, columns are dynamic and always available
|
|
134
|
+
// This method always returns true for Redis-based storage
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async scanKeys(pattern: string, batchSize = 10000): Promise<string[]> {
|
|
139
|
+
let cursor = '0';
|
|
140
|
+
let keys: string[] = [];
|
|
141
|
+
do {
|
|
142
|
+
const [nextCursor, batch] = await this.client.scan(cursor, {
|
|
143
|
+
match: pattern,
|
|
144
|
+
count: batchSize,
|
|
145
|
+
});
|
|
146
|
+
keys.push(...batch);
|
|
147
|
+
cursor = nextCursor;
|
|
148
|
+
} while (cursor !== '0');
|
|
149
|
+
return keys;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async scanAndDelete(pattern: string, batchSize = 10000): Promise<number> {
|
|
153
|
+
let cursor = '0';
|
|
154
|
+
let totalDeleted = 0;
|
|
155
|
+
do {
|
|
156
|
+
const [nextCursor, keys] = await this.client.scan(cursor, {
|
|
157
|
+
match: pattern,
|
|
158
|
+
count: batchSize,
|
|
159
|
+
});
|
|
160
|
+
if (keys.length > 0) {
|
|
161
|
+
await this.client.del(...keys);
|
|
162
|
+
totalDeleted += keys.length;
|
|
163
|
+
}
|
|
164
|
+
cursor = nextCursor;
|
|
165
|
+
} while (cursor !== '0');
|
|
166
|
+
return totalDeleted;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
2
|
+
import type { ScoreRowData } from '@mastra/core/scores';
|
|
3
|
+
import { ScoresStorage, TABLE_SCORERS } from '@mastra/core/storage';
|
|
4
|
+
import type { Redis } from '@upstash/redis';
|
|
5
|
+
import type { StoreOperationsUpstash } from '../operations';
|
|
6
|
+
import { processRecord } from '../utils';
|
|
7
|
+
|
|
8
|
+
function transformScoreRow(row: Record<string, any>): ScoreRowData {
|
|
9
|
+
const parseField = (v: any) => {
|
|
10
|
+
if (typeof v === 'string') {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(v);
|
|
13
|
+
} catch {
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
...row,
|
|
21
|
+
scorer: parseField(row.scorer),
|
|
22
|
+
extractStepResult: parseField(row.extractStepResult),
|
|
23
|
+
analyzeStepResult: parseField(row.analyzeStepResult),
|
|
24
|
+
metadata: parseField(row.metadata),
|
|
25
|
+
input: parseField(row.input),
|
|
26
|
+
output: parseField(row.output),
|
|
27
|
+
additionalContext: parseField(row.additionalContext),
|
|
28
|
+
runtimeContext: parseField(row.runtimeContext),
|
|
29
|
+
entity: parseField(row.entity),
|
|
30
|
+
createdAt: row.createdAt,
|
|
31
|
+
updatedAt: row.updatedAt,
|
|
32
|
+
} as ScoreRowData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ScoresUpstash extends ScoresStorage {
|
|
36
|
+
private client: Redis;
|
|
37
|
+
private operations: StoreOperationsUpstash;
|
|
38
|
+
|
|
39
|
+
constructor({ client, operations }: { client: Redis; operations: StoreOperationsUpstash }) {
|
|
40
|
+
super();
|
|
41
|
+
this.client = client;
|
|
42
|
+
this.operations = operations;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getScoreById({ id }: { id: string }): Promise<ScoreRowData | null> {
|
|
46
|
+
try {
|
|
47
|
+
const data = await this.operations.load<ScoreRowData>({
|
|
48
|
+
tableName: TABLE_SCORERS,
|
|
49
|
+
keys: { id },
|
|
50
|
+
});
|
|
51
|
+
if (!data) return null;
|
|
52
|
+
return transformScoreRow(data);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new MastraError(
|
|
55
|
+
{
|
|
56
|
+
id: 'STORAGE_UPSTASH_STORAGE_GET_SCORE_BY_ID_FAILED',
|
|
57
|
+
domain: ErrorDomain.STORAGE,
|
|
58
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
59
|
+
details: { id },
|
|
60
|
+
},
|
|
61
|
+
error,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getScoresByScorerId({
|
|
67
|
+
scorerId,
|
|
68
|
+
pagination = { page: 0, perPage: 20 },
|
|
69
|
+
}: {
|
|
70
|
+
scorerId: string;
|
|
71
|
+
pagination?: { page: number; perPage: number };
|
|
72
|
+
}): Promise<{
|
|
73
|
+
scores: ScoreRowData[];
|
|
74
|
+
pagination: { total: number; page: number; perPage: number; hasMore: boolean };
|
|
75
|
+
}> {
|
|
76
|
+
const pattern = `${TABLE_SCORERS}:*`;
|
|
77
|
+
const keys = await this.operations.scanKeys(pattern);
|
|
78
|
+
if (keys.length === 0) {
|
|
79
|
+
return {
|
|
80
|
+
scores: [],
|
|
81
|
+
pagination: { total: 0, page: pagination.page, perPage: pagination.perPage, hasMore: false },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const pipeline = this.client.pipeline();
|
|
85
|
+
keys.forEach(key => pipeline.get(key));
|
|
86
|
+
const results = await pipeline.exec();
|
|
87
|
+
// Filter out nulls and by scorerId
|
|
88
|
+
const filtered = results
|
|
89
|
+
.map((row: any) => row as Record<string, any> | null)
|
|
90
|
+
.filter((row): row is Record<string, any> => !!row && typeof row === 'object' && row.scorerId === scorerId);
|
|
91
|
+
const total = filtered.length;
|
|
92
|
+
const { page, perPage } = pagination;
|
|
93
|
+
const start = page * perPage;
|
|
94
|
+
const end = start + perPage;
|
|
95
|
+
const paged = filtered.slice(start, end);
|
|
96
|
+
const scores = paged.map(row => transformScoreRow(row));
|
|
97
|
+
return {
|
|
98
|
+
scores,
|
|
99
|
+
pagination: {
|
|
100
|
+
total,
|
|
101
|
+
page,
|
|
102
|
+
perPage,
|
|
103
|
+
hasMore: end < total,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async saveScore(score: ScoreRowData): Promise<{ score: ScoreRowData }> {
|
|
109
|
+
const { key, processedRecord } = processRecord(TABLE_SCORERS, score);
|
|
110
|
+
try {
|
|
111
|
+
await this.client.set(key, processedRecord);
|
|
112
|
+
return { score };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new MastraError(
|
|
115
|
+
{
|
|
116
|
+
id: 'STORAGE_UPSTASH_STORAGE_SAVE_SCORE_FAILED',
|
|
117
|
+
domain: ErrorDomain.STORAGE,
|
|
118
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
119
|
+
details: { id: score.id },
|
|
120
|
+
},
|
|
121
|
+
error,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getScoresByRunId({
|
|
127
|
+
runId,
|
|
128
|
+
pagination = { page: 0, perPage: 20 },
|
|
129
|
+
}: {
|
|
130
|
+
runId: string;
|
|
131
|
+
pagination?: { page: number; perPage: number };
|
|
132
|
+
}): Promise<{
|
|
133
|
+
scores: ScoreRowData[];
|
|
134
|
+
pagination: { total: number; page: number; perPage: number; hasMore: boolean };
|
|
135
|
+
}> {
|
|
136
|
+
const pattern = `${TABLE_SCORERS}:*`;
|
|
137
|
+
const keys = await this.operations.scanKeys(pattern);
|
|
138
|
+
if (keys.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
scores: [],
|
|
141
|
+
pagination: { total: 0, page: pagination.page, perPage: pagination.perPage, hasMore: false },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const pipeline = this.client.pipeline();
|
|
145
|
+
keys.forEach(key => pipeline.get(key));
|
|
146
|
+
const results = await pipeline.exec();
|
|
147
|
+
// Filter out nulls and by runId
|
|
148
|
+
const filtered = results
|
|
149
|
+
.map((row: any) => row as Record<string, any> | null)
|
|
150
|
+
.filter((row): row is Record<string, any> => !!row && typeof row === 'object' && row.runId === runId);
|
|
151
|
+
const total = filtered.length;
|
|
152
|
+
const { page, perPage } = pagination;
|
|
153
|
+
const start = page * perPage;
|
|
154
|
+
const end = start + perPage;
|
|
155
|
+
const paged = filtered.slice(start, end);
|
|
156
|
+
const scores = paged.map(row => transformScoreRow(row));
|
|
157
|
+
return {
|
|
158
|
+
scores,
|
|
159
|
+
pagination: {
|
|
160
|
+
total,
|
|
161
|
+
page,
|
|
162
|
+
perPage,
|
|
163
|
+
hasMore: end < total,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getScoresByEntityId({
|
|
169
|
+
entityId,
|
|
170
|
+
entityType,
|
|
171
|
+
pagination = { page: 0, perPage: 20 },
|
|
172
|
+
}: {
|
|
173
|
+
entityId: string;
|
|
174
|
+
entityType?: string;
|
|
175
|
+
pagination?: { page: number; perPage: number };
|
|
176
|
+
}): Promise<{
|
|
177
|
+
scores: ScoreRowData[];
|
|
178
|
+
pagination: { total: number; page: number; perPage: number; hasMore: boolean };
|
|
179
|
+
}> {
|
|
180
|
+
const pattern = `${TABLE_SCORERS}:*`;
|
|
181
|
+
const keys = await this.operations.scanKeys(pattern);
|
|
182
|
+
if (keys.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
scores: [],
|
|
185
|
+
pagination: { total: 0, page: pagination.page, perPage: pagination.perPage, hasMore: false },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const pipeline = this.client.pipeline();
|
|
189
|
+
keys.forEach(key => pipeline.get(key));
|
|
190
|
+
const results = await pipeline.exec();
|
|
191
|
+
|
|
192
|
+
const filtered = results
|
|
193
|
+
.map((row: any) => row as Record<string, any> | null)
|
|
194
|
+
.filter((row): row is Record<string, any> => {
|
|
195
|
+
if (!row || typeof row !== 'object') return false;
|
|
196
|
+
if (row.entityId !== entityId) return false;
|
|
197
|
+
if (entityType && row.entityType !== entityType) return false;
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
const total = filtered.length;
|
|
201
|
+
const { page, perPage } = pagination;
|
|
202
|
+
const start = page * perPage;
|
|
203
|
+
const end = start + perPage;
|
|
204
|
+
const paged = filtered.slice(start, end);
|
|
205
|
+
const scores = paged.map(row => transformScoreRow(row));
|
|
206
|
+
return {
|
|
207
|
+
scores,
|
|
208
|
+
pagination: {
|
|
209
|
+
total,
|
|
210
|
+
page,
|
|
211
|
+
perPage,
|
|
212
|
+
hasMore: end < total,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
2
|
+
import type { StorageGetTracesArg, PaginationInfo, PaginationArgs } from '@mastra/core/storage';
|
|
3
|
+
import { TracesStorage, TABLE_TRACES } from '@mastra/core/storage';
|
|
4
|
+
import type { Redis } from '@upstash/redis';
|
|
5
|
+
import type { StoreOperationsUpstash } from '../operations';
|
|
6
|
+
import { ensureDate, parseJSON } from '../utils';
|
|
7
|
+
|
|
8
|
+
export class TracesUpstash extends TracesStorage {
|
|
9
|
+
private client: Redis;
|
|
10
|
+
private operations: StoreOperationsUpstash;
|
|
11
|
+
|
|
12
|
+
constructor({ client, operations }: { client: Redis; operations: StoreOperationsUpstash }) {
|
|
13
|
+
super();
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.operations = operations;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated use getTracesPaginated instead
|
|
20
|
+
*/
|
|
21
|
+
public async getTraces(args: StorageGetTracesArg): Promise<any[]> {
|
|
22
|
+
if (args.fromDate || args.toDate) {
|
|
23
|
+
(args as any).dateRange = {
|
|
24
|
+
start: args.fromDate,
|
|
25
|
+
end: args.toDate,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const { traces } = await this.getTracesPaginated(args);
|
|
30
|
+
return traces;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new MastraError(
|
|
33
|
+
{
|
|
34
|
+
id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_FAILED',
|
|
35
|
+
domain: ErrorDomain.STORAGE,
|
|
36
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
37
|
+
},
|
|
38
|
+
error,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async getTracesPaginated(
|
|
44
|
+
args: {
|
|
45
|
+
name?: string;
|
|
46
|
+
scope?: string;
|
|
47
|
+
attributes?: Record<string, string>;
|
|
48
|
+
filters?: Record<string, any>;
|
|
49
|
+
} & PaginationArgs,
|
|
50
|
+
): Promise<PaginationInfo & { traces: any[] }> {
|
|
51
|
+
const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
|
|
52
|
+
const fromDate = dateRange?.start;
|
|
53
|
+
const toDate = dateRange?.end;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const pattern = `${TABLE_TRACES}:*`;
|
|
57
|
+
const keys = await this.operations.scanKeys(pattern);
|
|
58
|
+
|
|
59
|
+
if (keys.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
traces: [],
|
|
62
|
+
total: 0,
|
|
63
|
+
page,
|
|
64
|
+
perPage: perPage || 100,
|
|
65
|
+
hasMore: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pipeline = this.client.pipeline();
|
|
70
|
+
keys.forEach(key => pipeline.get(key));
|
|
71
|
+
const results = await pipeline.exec();
|
|
72
|
+
|
|
73
|
+
let filteredTraces = results.filter(
|
|
74
|
+
(record): record is Record<string, any> => record !== null && typeof record === 'object',
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (name) {
|
|
78
|
+
filteredTraces = filteredTraces.filter(record => record.name?.toLowerCase().startsWith(name.toLowerCase()));
|
|
79
|
+
}
|
|
80
|
+
if (scope) {
|
|
81
|
+
filteredTraces = filteredTraces.filter(record => record.scope === scope);
|
|
82
|
+
}
|
|
83
|
+
if (attributes) {
|
|
84
|
+
filteredTraces = filteredTraces.filter(record => {
|
|
85
|
+
const recordAttributes = record.attributes;
|
|
86
|
+
if (!recordAttributes) return false;
|
|
87
|
+
const parsedAttributes =
|
|
88
|
+
typeof recordAttributes === 'string' ? JSON.parse(recordAttributes) : recordAttributes;
|
|
89
|
+
return Object.entries(attributes).every(([key, value]) => parsedAttributes[key] === value);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (filters) {
|
|
93
|
+
filteredTraces = filteredTraces.filter(record =>
|
|
94
|
+
Object.entries(filters).every(([key, value]) => record[key] === value),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (fromDate) {
|
|
98
|
+
filteredTraces = filteredTraces.filter(
|
|
99
|
+
record => new Date(record.createdAt).getTime() >= new Date(fromDate).getTime(),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (toDate) {
|
|
103
|
+
filteredTraces = filteredTraces.filter(
|
|
104
|
+
record => new Date(record.createdAt).getTime() <= new Date(toDate).getTime(),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
filteredTraces.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
109
|
+
|
|
110
|
+
const transformedTraces = filteredTraces.map(record => ({
|
|
111
|
+
id: record.id,
|
|
112
|
+
parentSpanId: record.parentSpanId,
|
|
113
|
+
traceId: record.traceId,
|
|
114
|
+
name: record.name,
|
|
115
|
+
scope: record.scope,
|
|
116
|
+
kind: record.kind,
|
|
117
|
+
status: parseJSON(record.status),
|
|
118
|
+
events: parseJSON(record.events),
|
|
119
|
+
links: parseJSON(record.links),
|
|
120
|
+
attributes: parseJSON(record.attributes),
|
|
121
|
+
startTime: record.startTime,
|
|
122
|
+
endTime: record.endTime,
|
|
123
|
+
other: parseJSON(record.other),
|
|
124
|
+
createdAt: ensureDate(record.createdAt),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const total = transformedTraces.length;
|
|
128
|
+
const resolvedPerPage = perPage || 100;
|
|
129
|
+
const start = page * resolvedPerPage;
|
|
130
|
+
const end = start + resolvedPerPage;
|
|
131
|
+
const paginatedTraces = transformedTraces.slice(start, end);
|
|
132
|
+
const hasMore = end < total;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
traces: paginatedTraces,
|
|
136
|
+
total,
|
|
137
|
+
page,
|
|
138
|
+
perPage: resolvedPerPage,
|
|
139
|
+
hasMore,
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const mastraError = new MastraError(
|
|
143
|
+
{
|
|
144
|
+
id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_PAGINATED_FAILED',
|
|
145
|
+
domain: ErrorDomain.STORAGE,
|
|
146
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
147
|
+
details: {
|
|
148
|
+
name: args.name || '',
|
|
149
|
+
scope: args.scope || '',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
error,
|
|
153
|
+
);
|
|
154
|
+
this.logger?.trackException(mastraError);
|
|
155
|
+
this.logger.error(mastraError.toString());
|
|
156
|
+
return {
|
|
157
|
+
traces: [],
|
|
158
|
+
total: 0,
|
|
159
|
+
page,
|
|
160
|
+
perPage: perPage || 100,
|
|
161
|
+
hasMore: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async batchTraceInsert(args: { records: Record<string, any>[] }): Promise<void> {
|
|
167
|
+
return this.operations.batchInsert({
|
|
168
|
+
tableName: TABLE_TRACES,
|
|
169
|
+
records: args.records,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { serializeDate, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_EVALS } from '@mastra/core/storage';
|
|
2
|
+
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
3
|
+
|
|
4
|
+
export function ensureDate(value: any): Date | null {
|
|
5
|
+
if (!value) return null;
|
|
6
|
+
if (value instanceof Date) return value;
|
|
7
|
+
if (typeof value === 'string') return new Date(value);
|
|
8
|
+
if (typeof value === 'number') return new Date(value);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseJSON(value: any): any {
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value);
|
|
16
|
+
} catch {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getKey(tableName: TABLE_NAMES, keys: Record<string, any>): string {
|
|
24
|
+
const keyParts = Object.entries(keys)
|
|
25
|
+
.filter(([_, value]) => value !== undefined)
|
|
26
|
+
.map(([key, value]) => `${key}:${value}`);
|
|
27
|
+
return `${tableName}:${keyParts.join(':')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function processRecord(tableName: TABLE_NAMES, record: Record<string, any>) {
|
|
31
|
+
let key: string;
|
|
32
|
+
|
|
33
|
+
if (tableName === TABLE_MESSAGES) {
|
|
34
|
+
// For messages, use threadId as the primary key component
|
|
35
|
+
key = getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
36
|
+
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
37
|
+
key = getKey(tableName, {
|
|
38
|
+
namespace: record.namespace || 'workflows',
|
|
39
|
+
workflow_name: record.workflow_name,
|
|
40
|
+
run_id: record.run_id,
|
|
41
|
+
...(record.resourceId ? { resourceId: record.resourceId } : {}),
|
|
42
|
+
});
|
|
43
|
+
} else if (tableName === TABLE_EVALS) {
|
|
44
|
+
key = getKey(tableName, { id: record.run_id });
|
|
45
|
+
} else {
|
|
46
|
+
key = getKey(tableName, { id: record.id });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Convert dates to ISO strings before storing
|
|
50
|
+
const processedRecord = {
|
|
51
|
+
...record,
|
|
52
|
+
createdAt: serializeDate(record.createdAt),
|
|
53
|
+
updatedAt: serializeDate(record.updatedAt),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return { key, processedRecord };
|
|
57
|
+
}
|