@oneuptime/common 11.5.2 → 11.5.4
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/Models/DatabaseModels/Dashboard.ts +3 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.ts +1123 -380
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +1 -1
- package/Server/Services/DashboardService.ts +11 -2
- package/Server/Utils/Execute.ts +33 -13
- package/Server/Utils/Telemetry/LlmSpan.ts +38 -64
- package/Tests/Server/API/DashboardMasterPasswordAPI.test.ts +225 -0
- package/Types/Telemetry/LlmConventions.ts +182 -0
- package/UI/Components/Detail/PlaceholderText.tsx +1 -14
- package/build/dist/Models/DatabaseModels/Dashboard.js +3 -3
- package/build/dist/Models/DatabaseModels/Dashboard.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +1 -1
- package/build/dist/Server/Services/DashboardService.js +8 -2
- package/build/dist/Server/Services/DashboardService.js.map +1 -1
- package/build/dist/Server/Utils/Execute.js +21 -11
- package/build/dist/Server/Utils/Execute.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +14 -54
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmConventions.js +149 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -0
- package/build/dist/UI/Components/Detail/PlaceholderText.js +1 -4
- package/build/dist/UI/Components/Detail/PlaceholderText.js.map +1 -1
- package/package.json +1 -1
|
@@ -214,11 +214,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
214
214
|
const shouldEnforceMasterPassword: boolean = Boolean(
|
|
215
215
|
dashboard &&
|
|
216
216
|
dashboard.isPublicDashboard &&
|
|
217
|
-
dashboard.enableMasterPassword
|
|
218
|
-
dashboard.masterPassword,
|
|
217
|
+
dashboard.enableMasterPassword,
|
|
219
218
|
);
|
|
220
219
|
|
|
221
220
|
if (shouldEnforceMasterPassword) {
|
|
221
|
+
// Fail closed if protection was enabled before a password was set.
|
|
222
|
+
if (!dashboard?.masterPassword) {
|
|
223
|
+
return {
|
|
224
|
+
hasReadAccess: false,
|
|
225
|
+
error: new MasterPasswordRequiredException(
|
|
226
|
+
DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
222
231
|
const hasValidMasterPassword: boolean =
|
|
223
232
|
this.hasValidMasterPasswordCookie({
|
|
224
233
|
req,
|
package/Server/Utils/Execute.ts
CHANGED
|
@@ -27,22 +27,30 @@ export default class Execute {
|
|
|
27
27
|
{
|
|
28
28
|
...options,
|
|
29
29
|
},
|
|
30
|
-
(
|
|
30
|
+
(
|
|
31
|
+
err: ExecException | null,
|
|
32
|
+
stdout: string | Buffer,
|
|
33
|
+
stderr: string | Buffer,
|
|
34
|
+
) => {
|
|
35
|
+
// See executeCommandFile: string | Buffer for @types/node drift.
|
|
36
|
+
const stdoutText: string = stdout.toString();
|
|
37
|
+
const stderrText: string = stderr.toString();
|
|
38
|
+
|
|
31
39
|
if (err) {
|
|
32
40
|
logger.error(`Error executing command: ${command}`);
|
|
33
41
|
logger.error(err);
|
|
34
|
-
logger.error(
|
|
35
|
-
if (
|
|
36
|
-
logger.error(
|
|
42
|
+
logger.error(stdoutText);
|
|
43
|
+
if (stderrText) {
|
|
44
|
+
logger.error(stderrText);
|
|
37
45
|
}
|
|
38
46
|
return reject(err);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
if (
|
|
42
|
-
logger.debug(
|
|
49
|
+
if (stderrText) {
|
|
50
|
+
logger.debug(stderrText);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
return resolve(
|
|
53
|
+
return resolve(stdoutText);
|
|
46
54
|
},
|
|
47
55
|
);
|
|
48
56
|
},
|
|
@@ -81,22 +89,34 @@ export default class Execute {
|
|
|
81
89
|
? { timeout: data.timeoutInMS, killSignal: "SIGKILL" }
|
|
82
90
|
: {}),
|
|
83
91
|
},
|
|
84
|
-
(
|
|
92
|
+
(
|
|
93
|
+
err: ExecException | null,
|
|
94
|
+
stdout: string | Buffer,
|
|
95
|
+
stderr: string | Buffer,
|
|
96
|
+
) => {
|
|
97
|
+
/*
|
|
98
|
+
* Newer @types/node type execFile's callback output as
|
|
99
|
+
* string | Buffer — coerce so this compiles on both the pinned
|
|
100
|
+
* and freshly-resolved typings (caret ranges drift in CI).
|
|
101
|
+
*/
|
|
102
|
+
const stdoutText: string = stdout.toString();
|
|
103
|
+
const stderrText: string = stderr.toString();
|
|
104
|
+
|
|
85
105
|
if (err) {
|
|
86
106
|
logger.error(
|
|
87
107
|
`Error executing command: ${data.command} ${data.args.join(" ")}`,
|
|
88
108
|
);
|
|
89
109
|
logger.error(err);
|
|
90
|
-
logger.error(
|
|
91
|
-
logger.error(
|
|
110
|
+
logger.error(stdoutText);
|
|
111
|
+
logger.error(stderrText);
|
|
92
112
|
return reject(err);
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
if (
|
|
96
|
-
logger.debug(
|
|
115
|
+
if (stderrText) {
|
|
116
|
+
logger.debug(stderrText);
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
return resolve(
|
|
119
|
+
return resolve(stdoutText);
|
|
100
120
|
},
|
|
101
121
|
);
|
|
102
122
|
},
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import Dictionary from "../../../Types/Dictionary";
|
|
2
|
+
import {
|
|
3
|
+
LlmAgentNameAttributeKeys,
|
|
4
|
+
LlmAttributeNamespacePrefixes,
|
|
5
|
+
LlmCostAttributeKeys,
|
|
6
|
+
LlmInputTokenAttributeKeys,
|
|
7
|
+
LlmOperationAttributeKeys,
|
|
8
|
+
LlmOutputTokenAttributeKeys,
|
|
9
|
+
LlmRequestModelAttributeKeys,
|
|
10
|
+
LlmResponseModelAttributeKeys,
|
|
11
|
+
LlmSystemAttributeKeys,
|
|
12
|
+
LlmToolNameAttributeKeys,
|
|
13
|
+
LlmTotalTokenAttributeKeys,
|
|
14
|
+
} from "../../../Types/Telemetry/LlmConventions";
|
|
2
15
|
import { AttributeType } from "./Telemetry";
|
|
3
16
|
|
|
4
17
|
/*
|
|
@@ -7,11 +20,9 @@ import { AttributeType } from "./Telemetry";
|
|
|
7
20
|
* OneUptime ingests OpenTelemetry spans generically. To make LLM and agent
|
|
8
21
|
* telemetry a first-class signal (filterable lists, token/cost/latency
|
|
9
22
|
* rollups) we denormalize a small set of values out of the span attributes at
|
|
10
|
-
* ingest time.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - OpenLLMetry / Traceloop (gen_ai.* + traceloop.*)
|
|
14
|
-
* - OpenInference / Arize (llm.* + openinference.span.kind)
|
|
23
|
+
* ingest time. The set of recognized attribute keys lives in the shared
|
|
24
|
+
* Common/Types/Telemetry/LlmConventions module so this server-side extractor
|
|
25
|
+
* and the client-side display parser cannot drift out of sync.
|
|
15
26
|
*
|
|
16
27
|
* Prompt/completion CONTENT is intentionally NOT denormalized here — it stays
|
|
17
28
|
* in the span's attributes/events map (already captured + scrubbed) and is
|
|
@@ -79,29 +90,19 @@ export default class LlmSpanUtil {
|
|
|
79
90
|
return fields;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
fields.llmSystem = this.getString(attributes,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fields.llmRequestModel = this.getString(attributes, [
|
|
96
|
-
"gen_ai.request.model",
|
|
97
|
-
"llm.model_name",
|
|
98
|
-
"llm.request.model",
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
fields.llmResponseModel = this.getString(attributes, [
|
|
102
|
-
"gen_ai.response.model",
|
|
103
|
-
"llm.response.model",
|
|
104
|
-
]);
|
|
93
|
+
fields.llmSystem = this.getString(attributes, LlmSystemAttributeKeys);
|
|
94
|
+
|
|
95
|
+
fields.llmOperation = this.getString(attributes, LlmOperationAttributeKeys);
|
|
96
|
+
|
|
97
|
+
fields.llmRequestModel = this.getString(
|
|
98
|
+
attributes,
|
|
99
|
+
LlmRequestModelAttributeKeys,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
fields.llmResponseModel = this.getString(
|
|
103
|
+
attributes,
|
|
104
|
+
LlmResponseModelAttributeKeys,
|
|
105
|
+
);
|
|
105
106
|
|
|
106
107
|
// Fall back to the response model when no request model was reported.
|
|
107
108
|
if (!fields.llmRequestModel && fields.llmResponseModel) {
|
|
@@ -114,29 +115,15 @@ export default class LlmSpanUtil {
|
|
|
114
115
|
* reject the row and fail the whole span batch.
|
|
115
116
|
*/
|
|
116
117
|
fields.llmInputTokens = Math.trunc(
|
|
117
|
-
this.getNumber(attributes,
|
|
118
|
-
"gen_ai.usage.input_tokens",
|
|
119
|
-
"gen_ai.usage.prompt_tokens",
|
|
120
|
-
"llm.token_count.prompt",
|
|
121
|
-
"llm.usage.prompt_tokens",
|
|
122
|
-
]),
|
|
118
|
+
this.getNumber(attributes, LlmInputTokenAttributeKeys),
|
|
123
119
|
);
|
|
124
120
|
|
|
125
121
|
fields.llmOutputTokens = Math.trunc(
|
|
126
|
-
this.getNumber(attributes,
|
|
127
|
-
"gen_ai.usage.output_tokens",
|
|
128
|
-
"gen_ai.usage.completion_tokens",
|
|
129
|
-
"llm.token_count.completion",
|
|
130
|
-
"llm.usage.completion_tokens",
|
|
131
|
-
]),
|
|
122
|
+
this.getNumber(attributes, LlmOutputTokenAttributeKeys),
|
|
132
123
|
);
|
|
133
124
|
|
|
134
125
|
fields.llmTotalTokens = Math.trunc(
|
|
135
|
-
this.getNumber(attributes,
|
|
136
|
-
"gen_ai.usage.total_tokens",
|
|
137
|
-
"llm.token_count.total",
|
|
138
|
-
"llm.usage.total_tokens",
|
|
139
|
-
]),
|
|
126
|
+
this.getNumber(attributes, LlmTotalTokenAttributeKeys),
|
|
140
127
|
);
|
|
141
128
|
|
|
142
129
|
// Derive total when only the parts were reported.
|
|
@@ -147,22 +134,11 @@ export default class LlmSpanUtil {
|
|
|
147
134
|
fields.llmTotalTokens = fields.llmInputTokens + fields.llmOutputTokens;
|
|
148
135
|
}
|
|
149
136
|
|
|
150
|
-
fields.llmCost = this.getNumber(attributes,
|
|
151
|
-
"gen_ai.usage.cost",
|
|
152
|
-
"gen_ai.usage.cost_usd",
|
|
153
|
-
"gen_ai.usage.total_cost",
|
|
154
|
-
"llm.usage.total_cost",
|
|
155
|
-
]);
|
|
137
|
+
fields.llmCost = this.getNumber(attributes, LlmCostAttributeKeys);
|
|
156
138
|
|
|
157
|
-
fields.llmAgentName = this.getString(attributes,
|
|
158
|
-
"gen_ai.agent.name",
|
|
159
|
-
"agent.name",
|
|
160
|
-
]);
|
|
139
|
+
fields.llmAgentName = this.getString(attributes, LlmAgentNameAttributeKeys);
|
|
161
140
|
|
|
162
|
-
fields.llmToolName = this.getString(attributes,
|
|
163
|
-
"gen_ai.tool.name",
|
|
164
|
-
"tool.name",
|
|
165
|
-
]);
|
|
141
|
+
fields.llmToolName = this.getString(attributes, LlmToolNameAttributeKeys);
|
|
166
142
|
|
|
167
143
|
fields.isLlmSpan = this.detectIsLlmSpan(keys, fields);
|
|
168
144
|
|
|
@@ -187,11 +163,9 @@ export default class LlmSpanUtil {
|
|
|
187
163
|
|
|
188
164
|
// Last-resort: any GenAI/LLM-namespaced attribute at all.
|
|
189
165
|
return keys.some((key: string) => {
|
|
190
|
-
return (
|
|
191
|
-
key.startsWith(
|
|
192
|
-
|
|
193
|
-
key.startsWith("traceloop.")
|
|
194
|
-
);
|
|
166
|
+
return LlmAttributeNamespacePrefixes.some((prefix: string) => {
|
|
167
|
+
return key.startsWith(prefix);
|
|
168
|
+
});
|
|
195
169
|
});
|
|
196
170
|
}
|
|
197
171
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import Dashboard from "../../../Models/DatabaseModels/Dashboard";
|
|
2
|
+
import DashboardAPI from "../../../Server/API/DashboardAPI";
|
|
3
|
+
import { EncryptionSecret } from "../../../Server/EnvironmentConfig";
|
|
4
|
+
import DashboardService from "../../../Server/Services/DashboardService";
|
|
5
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
6
|
+
import {
|
|
7
|
+
ExpressRequest,
|
|
8
|
+
ExpressResponse,
|
|
9
|
+
NextFunction,
|
|
10
|
+
} from "../../../Server/Utils/Express";
|
|
11
|
+
import Response from "../../../Server/Utils/Response";
|
|
12
|
+
import {
|
|
13
|
+
DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE,
|
|
14
|
+
DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
|
|
15
|
+
} from "../../../Types/Dashboard/MasterPassword";
|
|
16
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
17
|
+
import MasterPasswordRequiredException from "../../../Types/Exception/MasterPasswordRequiredException";
|
|
18
|
+
import HashedString from "../../../Types/HashedString";
|
|
19
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
20
|
+
import { mockRouter } from "./Helpers";
|
|
21
|
+
import {
|
|
22
|
+
afterEach,
|
|
23
|
+
beforeAll,
|
|
24
|
+
beforeEach,
|
|
25
|
+
describe,
|
|
26
|
+
expect,
|
|
27
|
+
it,
|
|
28
|
+
} from "@jest/globals";
|
|
29
|
+
|
|
30
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
31
|
+
return {
|
|
32
|
+
getRouter: () => {
|
|
33
|
+
return mockRouter;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
39
|
+
return {
|
|
40
|
+
sendEntityArrayResponse: jest.fn().mockImplementation((...args: []) => {
|
|
41
|
+
return args;
|
|
42
|
+
}),
|
|
43
|
+
sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
|
|
44
|
+
return args;
|
|
45
|
+
}),
|
|
46
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
47
|
+
sendEntityResponse: jest.fn().mockImplementation((...args: []) => {
|
|
48
|
+
return args;
|
|
49
|
+
}),
|
|
50
|
+
sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
|
|
51
|
+
return args;
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("DashboardAPI master password", () => {
|
|
57
|
+
const password: string = "correct horse battery staple";
|
|
58
|
+
|
|
59
|
+
let dashboardId: ObjectID;
|
|
60
|
+
let dashboard: Dashboard;
|
|
61
|
+
let mockRequest: ExpressRequest;
|
|
62
|
+
let mockResponse: ExpressResponse;
|
|
63
|
+
let nextFunction: NextFunction;
|
|
64
|
+
|
|
65
|
+
beforeAll(() => {
|
|
66
|
+
mockRouter.routes.length = 0;
|
|
67
|
+
new DashboardAPI();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
|
|
73
|
+
dashboardId = ObjectID.generate();
|
|
74
|
+
dashboard = new Dashboard();
|
|
75
|
+
dashboard.id = dashboardId;
|
|
76
|
+
dashboard.isPublicDashboard = true;
|
|
77
|
+
dashboard.enableMasterPassword = true;
|
|
78
|
+
dashboard.masterPassword = new HashedString(
|
|
79
|
+
await HashedString.hashValue(password, EncryptionSecret),
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
jest.spyOn(DashboardService, "findOneById").mockResolvedValue(dashboard);
|
|
84
|
+
|
|
85
|
+
mockRequest = {
|
|
86
|
+
params: {
|
|
87
|
+
dashboardId: dashboardId.toString(),
|
|
88
|
+
},
|
|
89
|
+
body: {
|
|
90
|
+
password,
|
|
91
|
+
},
|
|
92
|
+
cookies: {},
|
|
93
|
+
headers: {},
|
|
94
|
+
socket: {},
|
|
95
|
+
ips: [],
|
|
96
|
+
} as unknown as ExpressRequest;
|
|
97
|
+
|
|
98
|
+
mockResponse = {
|
|
99
|
+
cookie: jest.fn(),
|
|
100
|
+
send: jest.fn(),
|
|
101
|
+
json: jest.fn(),
|
|
102
|
+
status: jest.fn().mockReturnThis(),
|
|
103
|
+
} as unknown as ExpressResponse;
|
|
104
|
+
|
|
105
|
+
nextFunction = jest.fn();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
jest.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("denies a protected public dashboard when no master-password cookie is present", async () => {
|
|
113
|
+
const result: Awaited<ReturnType<typeof DashboardService.hasReadAccess>> =
|
|
114
|
+
await DashboardService.hasReadAccess({
|
|
115
|
+
dashboardId,
|
|
116
|
+
req: mockRequest,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result.hasReadAccess).toBe(false);
|
|
120
|
+
expect(result.error).toBeInstanceOf(MasterPasswordRequiredException);
|
|
121
|
+
expect(result.error?.message).toBe(
|
|
122
|
+
DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("fails closed when master-password protection is enabled without a stored password", async () => {
|
|
127
|
+
delete dashboard.masterPassword;
|
|
128
|
+
|
|
129
|
+
const result: Awaited<ReturnType<typeof DashboardService.hasReadAccess>> =
|
|
130
|
+
await DashboardService.hasReadAccess({
|
|
131
|
+
dashboardId,
|
|
132
|
+
req: mockRequest,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.hasReadAccess).toBe(false);
|
|
136
|
+
expect(result.error).toBeInstanceOf(MasterPasswordRequiredException);
|
|
137
|
+
expect(result.error?.message).toBe(
|
|
138
|
+
DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("issues a dashboard-scoped cookie for the correct password and unlocks only that dashboard", async () => {
|
|
143
|
+
await mockRouter
|
|
144
|
+
.match("post", "/dashboard/master-password/:dashboardId")
|
|
145
|
+
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
146
|
+
|
|
147
|
+
expect(nextFunction).not.toHaveBeenCalled();
|
|
148
|
+
expect(Response.sendEmptySuccessResponse).toHaveBeenCalledWith(
|
|
149
|
+
mockRequest,
|
|
150
|
+
mockResponse,
|
|
151
|
+
);
|
|
152
|
+
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
|
153
|
+
|
|
154
|
+
const cookieCall: Array<unknown> = (mockResponse.cookie as jest.Mock).mock
|
|
155
|
+
.calls[0] as Array<unknown>;
|
|
156
|
+
const cookieName: string = cookieCall[0] as string;
|
|
157
|
+
const cookieToken: string = cookieCall[1] as string;
|
|
158
|
+
|
|
159
|
+
expect(cookieName).toBe(
|
|
160
|
+
CookieUtil.getDashboardMasterPasswordKey(dashboardId),
|
|
161
|
+
);
|
|
162
|
+
expect(cookieToken).toEqual(expect.any(String));
|
|
163
|
+
|
|
164
|
+
const unlockedRequest: ExpressRequest = {
|
|
165
|
+
cookies: {
|
|
166
|
+
[cookieName]: cookieToken,
|
|
167
|
+
},
|
|
168
|
+
headers: {},
|
|
169
|
+
socket: {},
|
|
170
|
+
ips: [],
|
|
171
|
+
} as unknown as ExpressRequest;
|
|
172
|
+
|
|
173
|
+
const unlockedResult: Awaited<
|
|
174
|
+
ReturnType<typeof DashboardService.hasReadAccess>
|
|
175
|
+
> = await DashboardService.hasReadAccess({
|
|
176
|
+
dashboardId,
|
|
177
|
+
req: unlockedRequest,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(unlockedResult.hasReadAccess).toBe(true);
|
|
181
|
+
expect(unlockedResult.error).toBeUndefined();
|
|
182
|
+
|
|
183
|
+
const otherDashboardId: ObjectID = ObjectID.generate();
|
|
184
|
+
const copiedCookieRequest: ExpressRequest = {
|
|
185
|
+
cookies: {
|
|
186
|
+
[CookieUtil.getDashboardMasterPasswordKey(otherDashboardId)]:
|
|
187
|
+
cookieToken,
|
|
188
|
+
},
|
|
189
|
+
headers: {},
|
|
190
|
+
socket: {},
|
|
191
|
+
ips: [],
|
|
192
|
+
} as unknown as ExpressRequest;
|
|
193
|
+
|
|
194
|
+
const isolatedResult: Awaited<
|
|
195
|
+
ReturnType<typeof DashboardService.hasReadAccess>
|
|
196
|
+
> = await DashboardService.hasReadAccess({
|
|
197
|
+
dashboardId: otherDashboardId,
|
|
198
|
+
req: copiedCookieRequest,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(isolatedResult.hasReadAccess).toBe(false);
|
|
202
|
+
expect(isolatedResult.error).toBeInstanceOf(
|
|
203
|
+
MasterPasswordRequiredException,
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("rejects an incorrect password without issuing a cookie", async () => {
|
|
208
|
+
mockRequest.body["password"] = "incorrect password";
|
|
209
|
+
|
|
210
|
+
await mockRouter
|
|
211
|
+
.match("post", "/dashboard/master-password/:dashboardId")
|
|
212
|
+
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
213
|
+
|
|
214
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
215
|
+
|
|
216
|
+
const error: unknown = (nextFunction as jest.Mock).mock.calls[0]?.[0];
|
|
217
|
+
|
|
218
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
219
|
+
expect((error as BadDataException).message).toBe(
|
|
220
|
+
DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE,
|
|
221
|
+
);
|
|
222
|
+
expect(mockResponse.cookie).not.toHaveBeenCalled();
|
|
223
|
+
expect(Response.sendEmptySuccessResponse).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Single source of truth for the OpenTelemetry GenAI (gen_ai.*) semantic
|
|
3
|
+
* convention attribute keys OneUptime recognizes when it detects and
|
|
4
|
+
* denormalizes LLM / GenAI / agent telemetry, plus cheap fallbacks for the two
|
|
5
|
+
* dominant instrumentation libraries:
|
|
6
|
+
* - OpenLLMetry / Traceloop (gen_ai.* + traceloop.*)
|
|
7
|
+
* - OpenInference / Arize (llm.* + openinference.span.kind)
|
|
8
|
+
*
|
|
9
|
+
* Both the server-side ingest extractor
|
|
10
|
+
* (Common/Server/Utils/Telemetry/LlmSpan.ts) and the client-side display parser
|
|
11
|
+
* (App/FeatureSet/Dashboard/src/Utils/LlmSpanDisplay.ts) import these lists so
|
|
12
|
+
* the two cannot silently drift apart when a new attribute is added — add a
|
|
13
|
+
* newly recognized key HERE, once.
|
|
14
|
+
*
|
|
15
|
+
* Order matters: within each list the preferred convention comes first and the
|
|
16
|
+
* lookup helpers return the first key that is present.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Provider / system, e.g. "openai", "anthropic", "aws.bedrock".
|
|
20
|
+
export const LlmSystemAttributeKeys: Array<string> = [
|
|
21
|
+
"gen_ai.system",
|
|
22
|
+
"gen_ai.provider.name",
|
|
23
|
+
"llm.system",
|
|
24
|
+
"llm.provider",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Operation, e.g. "chat", "embeddings", "execute_tool", "invoke_agent".
|
|
28
|
+
export const LlmOperationAttributeKeys: Array<string> = [
|
|
29
|
+
"gen_ai.operation.name",
|
|
30
|
+
"llm.request.type",
|
|
31
|
+
"openinference.span.kind",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Model requested by the caller.
|
|
35
|
+
export const LlmRequestModelAttributeKeys: Array<string> = [
|
|
36
|
+
"gen_ai.request.model",
|
|
37
|
+
"llm.model_name",
|
|
38
|
+
"llm.request.model",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Model the provider actually served (often the resolved/pinned model).
|
|
42
|
+
export const LlmResponseModelAttributeKeys: Array<string> = [
|
|
43
|
+
"gen_ai.response.model",
|
|
44
|
+
"llm.response.model",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const LlmInputTokenAttributeKeys: Array<string> = [
|
|
48
|
+
"gen_ai.usage.input_tokens",
|
|
49
|
+
"gen_ai.usage.prompt_tokens",
|
|
50
|
+
"llm.token_count.prompt",
|
|
51
|
+
"llm.usage.prompt_tokens",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const LlmOutputTokenAttributeKeys: Array<string> = [
|
|
55
|
+
"gen_ai.usage.output_tokens",
|
|
56
|
+
"gen_ai.usage.completion_tokens",
|
|
57
|
+
"llm.token_count.completion",
|
|
58
|
+
"llm.usage.completion_tokens",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const LlmTotalTokenAttributeKeys: Array<string> = [
|
|
62
|
+
"gen_ai.usage.total_tokens",
|
|
63
|
+
"llm.token_count.total",
|
|
64
|
+
"llm.usage.total_tokens",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Cost in USD. Only populated when the SDK reports it (no built-in pricing).
|
|
68
|
+
export const LlmCostAttributeKeys: Array<string> = [
|
|
69
|
+
"gen_ai.usage.cost",
|
|
70
|
+
"gen_ai.usage.cost_usd",
|
|
71
|
+
"gen_ai.usage.total_cost",
|
|
72
|
+
"llm.usage.total_cost",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const LlmAgentNameAttributeKeys: Array<string> = [
|
|
76
|
+
"gen_ai.agent.name",
|
|
77
|
+
"agent.name",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export const LlmToolNameAttributeKeys: Array<string> = [
|
|
81
|
+
"gen_ai.tool.name",
|
|
82
|
+
"tool.name",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* Request-parameter keys — surfaced only in the display panel, never
|
|
87
|
+
* denormalized to DB columns.
|
|
88
|
+
*/
|
|
89
|
+
export const LlmTemperatureAttributeKeys: Array<string> = [
|
|
90
|
+
"gen_ai.request.temperature",
|
|
91
|
+
"llm.request.temperature",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export const LlmMaxTokensAttributeKeys: Array<string> = [
|
|
95
|
+
"gen_ai.request.max_tokens",
|
|
96
|
+
"llm.request.max_tokens",
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export const LlmTopPAttributeKeys: Array<string> = [
|
|
100
|
+
"gen_ai.request.top_p",
|
|
101
|
+
"llm.request.top_p",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export const LlmFinishReasonAttributeKeys: Array<string> = [
|
|
105
|
+
"gen_ai.response.finish_reasons",
|
|
106
|
+
"gen_ai.response.finish_reason",
|
|
107
|
+
"llm.response.finish_reason",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Attribute-key namespace prefixes. Any span carrying an attribute in one of
|
|
112
|
+
* these namespaces is treated as an LLM/GenAI span as a last resort.
|
|
113
|
+
*/
|
|
114
|
+
export const LlmAttributeNamespacePrefixes: Array<string> = [
|
|
115
|
+
"gen_ai.",
|
|
116
|
+
"llm.",
|
|
117
|
+
"traceloop.",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* Indexed prompt/completion message conventions of the shape
|
|
122
|
+
* `${prefix}.${i}.${contentSuffix}` / `${prefix}.${i}.${roleSuffix}`. Used only
|
|
123
|
+
* by the display parser to reconstruct message content for rendering.
|
|
124
|
+
*/
|
|
125
|
+
export interface LlmIndexedMessageConvention {
|
|
126
|
+
prefix: string;
|
|
127
|
+
contentSuffix: string;
|
|
128
|
+
roleSuffix: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const LlmPromptIndexedMessageConventions: Array<LlmIndexedMessageConvention> =
|
|
132
|
+
[
|
|
133
|
+
// OpenLLMetry indexed prompts.
|
|
134
|
+
{ prefix: "gen_ai.prompt", contentSuffix: "content", roleSuffix: "role" },
|
|
135
|
+
// OpenInference indexed input messages.
|
|
136
|
+
{
|
|
137
|
+
prefix: "llm.input_messages",
|
|
138
|
+
contentSuffix: "message.content",
|
|
139
|
+
roleSuffix: "message.role",
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export const LlmCompletionIndexedMessageConventions: Array<LlmIndexedMessageConvention> =
|
|
144
|
+
[
|
|
145
|
+
// OpenLLMetry indexed completions.
|
|
146
|
+
{
|
|
147
|
+
prefix: "gen_ai.completion",
|
|
148
|
+
contentSuffix: "content",
|
|
149
|
+
roleSuffix: "role",
|
|
150
|
+
},
|
|
151
|
+
// OpenInference indexed output messages.
|
|
152
|
+
{
|
|
153
|
+
prefix: "llm.output_messages",
|
|
154
|
+
contentSuffix: "message.content",
|
|
155
|
+
roleSuffix: "message.role",
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// JSON-encoded message-array attribute keys (checked in order).
|
|
160
|
+
export const LlmPromptJsonAttributeKeys: Array<string> = [
|
|
161
|
+
"gen_ai.input.messages",
|
|
162
|
+
"gen_ai.prompt",
|
|
163
|
+
"input.value",
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
export const LlmCompletionJsonAttributeKeys: Array<string> = [
|
|
167
|
+
"gen_ai.output.messages",
|
|
168
|
+
"gen_ai.completion",
|
|
169
|
+
"output.value",
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
// Span-event names carrying prompt/completion content.
|
|
173
|
+
export const LlmPromptEventNames: Array<string> = [
|
|
174
|
+
"gen_ai.system.message",
|
|
175
|
+
"gen_ai.user.message",
|
|
176
|
+
"gen_ai.tool.message",
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
export const LlmCompletionEventNames: Array<string> = [
|
|
180
|
+
"gen_ai.assistant.message",
|
|
181
|
+
"gen_ai.choice",
|
|
182
|
+
];
|
|
@@ -11,20 +11,7 @@ const PlaceholderText: FunctionComponent<ComponentProps> = (
|
|
|
11
11
|
const { translateString } = useTranslateValue();
|
|
12
12
|
const translatedText: string = translateString(props.text) ?? props.text;
|
|
13
13
|
return (
|
|
14
|
-
<span className="inline-flex items-center
|
|
15
|
-
<svg
|
|
16
|
-
className="w-3.5 h-3.5 text-gray-300"
|
|
17
|
-
fill="none"
|
|
18
|
-
viewBox="0 0 24 24"
|
|
19
|
-
stroke="currentColor"
|
|
20
|
-
>
|
|
21
|
-
<path
|
|
22
|
-
strokeLinecap="round"
|
|
23
|
-
strokeLinejoin="round"
|
|
24
|
-
strokeWidth={1.5}
|
|
25
|
-
d="M20 12H4"
|
|
26
|
-
/>
|
|
27
|
-
</svg>
|
|
14
|
+
<span className="inline-flex items-center whitespace-nowrap rounded-md border border-dashed border-gray-300 bg-gray-50 px-2 py-0.5 align-middle text-sm font-normal text-gray-500 select-none">
|
|
28
15
|
{translatedText}
|
|
29
16
|
</span>
|
|
30
17
|
);
|