@pranshulsoni/flowwatch 1.0.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/README.md +442 -0
- package/dist/ai/groqInsightService.d.ts +39 -0
- package/dist/ai/groqInsightService.js +230 -0
- package/dist/createFlowwatch.d.ts +17 -0
- package/dist/createFlowwatch.js +90 -0
- package/dist/dashboard/routes/dashboardResponse.d.ts +204 -0
- package/dist/dashboard/routes/dashboardResponse.js +248 -0
- package/dist/dashboard/routes/router.d.ts +13 -0
- package/dist/dashboard/routes/router.js +708 -0
- package/dist/dashboard/static/dashboard.html +6061 -0
- package/dist/engine/background/queues/workflowQueue.d.ts +6 -0
- package/dist/engine/background/queues/workflowQueue.js +14 -0
- package/dist/engine/background/workers/workflowWorker.d.ts +15 -0
- package/dist/engine/background/workers/workflowWorker.js +98 -0
- package/dist/engine/errors/errorEngine.d.ts +27 -0
- package/dist/engine/errors/errorEngine.js +115 -0
- package/dist/engine/flags/evaluateFlag.d.ts +3 -0
- package/dist/engine/flags/evaluateFlag.js +50 -0
- package/dist/engine/flags/flagEngine.d.ts +9 -0
- package/dist/engine/flags/flagEngine.js +52 -0
- package/dist/engine/flags/hashRollout.d.ts +1 -0
- package/dist/engine/flags/hashRollout.js +9 -0
- package/dist/engine/flags/types.d.ts +7 -0
- package/dist/engine/flags/types.js +1 -0
- package/dist/engine/trace/traceEngine.d.ts +26 -0
- package/dist/engine/trace/traceEngine.js +76 -0
- package/dist/engine/workflows/types.d.ts +28 -0
- package/dist/engine/workflows/types.js +1 -0
- package/dist/engine/workflows/workflowEngine.d.ts +15 -0
- package/dist/engine/workflows/workflowEngine.js +112 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3 -0
- package/dist/persistence/cache/redisClient.d.ts +2 -0
- package/dist/persistence/cache/redisClient.js +4 -0
- package/dist/persistence/db/postgres.d.ts +3 -0
- package/dist/persistence/db/postgres.js +4 -0
- package/dist/persistence/migrations/migrationRunner.d.ts +3 -0
- package/dist/persistence/migrations/migrationRunner.js +46 -0
- package/dist/persistence/migrations/migrations.d.ts +5 -0
- package/dist/persistence/migrations/migrations.js +191 -0
- package/dist/persistence/repositories/errors/errorRepository.d.ts +38 -0
- package/dist/persistence/repositories/errors/errorRepository.js +63 -0
- package/dist/persistence/repositories/flags/flagRepository.d.ts +72 -0
- package/dist/persistence/repositories/flags/flagRepository.js +245 -0
- package/dist/persistence/repositories/traces/traceRepository.d.ts +64 -0
- package/dist/persistence/repositories/traces/traceRepository.js +110 -0
- package/dist/persistence/repositories/workflows/workflowRepository.d.ts +93 -0
- package/dist/persistence/repositories/workflows/workflowRepository.js +260 -0
- package/dist/persistence/transaction.d.ts +2 -0
- package/dist/persistence/transaction.js +16 -0
- package/dist/runtime/config/normalizeConfig.d.ts +2 -0
- package/dist/runtime/config/normalizeConfig.js +46 -0
- package/dist/runtime/config/validationConfig.d.ts +2 -0
- package/dist/runtime/config/validationConfig.js +119 -0
- package/dist/runtime/health/healthService.d.ts +30 -0
- package/dist/runtime/health/healthService.js +54 -0
- package/dist/runtime/tracing/traceContext.d.ts +12 -0
- package/dist/runtime/tracing/traceContext.js +28 -0
- package/dist/runtime/tracing/tracingMiddleware.d.ts +3 -0
- package/dist/runtime/tracing/tracingMiddleware.js +46 -0
- package/dist/search/elasticsearch/client.d.ts +2 -0
- package/dist/search/elasticsearch/client.js +4 -0
- package/dist/search/elasticsearch/indexSetup.d.ts +3 -0
- package/dist/search/elasticsearch/indexSetup.js +43 -0
- package/dist/search/elasticsearch/indexer.d.ts +9 -0
- package/dist/search/elasticsearch/indexer.js +86 -0
- package/dist/search/elasticsearch/mappingChecker.d.ts +2 -0
- package/dist/search/elasticsearch/mappingChecker.js +28 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/flowwatchEnvStore.d.ts +27 -0
- package/dist/utils/flowwatchEnvStore.js +145 -0
- package/package.json +63 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { insertWorkflow, insertWorkflowExecution } from "../../persistence/repositories/workflows/workflowRepository.js";
|
|
2
|
+
import { addWorkflowJobToQueue } from "../background/queues/workflowQueue.js";
|
|
3
|
+
export function createWorkflowEngine(options) {
|
|
4
|
+
const { pool, workflowQueue, traceEngine } = options;
|
|
5
|
+
const registry = new Map();
|
|
6
|
+
async function workflow(name, steps) {
|
|
7
|
+
return traceEngine.trace("flowwatch.workflow.register", "workflow_step", async () => {
|
|
8
|
+
validateWorkflow(name, steps);
|
|
9
|
+
const workflowSteps = [];
|
|
10
|
+
for (const step of steps) {
|
|
11
|
+
workflowSteps.push({
|
|
12
|
+
name: step.name,
|
|
13
|
+
maxRetries: step.retries ?? 0,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const insertedWorkflow = await insertWorkflow(pool, {
|
|
17
|
+
name,
|
|
18
|
+
steps: workflowSteps,
|
|
19
|
+
});
|
|
20
|
+
const registeredWorkflow = {
|
|
21
|
+
name,
|
|
22
|
+
steps,
|
|
23
|
+
dbWorkflow: insertedWorkflow.workflow,
|
|
24
|
+
dbSteps: insertedWorkflow.steps,
|
|
25
|
+
};
|
|
26
|
+
registry.set(name, registeredWorkflow);
|
|
27
|
+
}, {
|
|
28
|
+
workflowName: name,
|
|
29
|
+
stepCount: steps.length,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function getWorkflow(name) {
|
|
33
|
+
return registry.get(name);
|
|
34
|
+
}
|
|
35
|
+
async function trigger(name, input) {
|
|
36
|
+
return traceEngine.trace("flowwatch.workflow.trigger", "workflow_step", async () => {
|
|
37
|
+
const workflow = getWorkflow(name);
|
|
38
|
+
if (!workflow) {
|
|
39
|
+
throw new Error(`workflow not found: ${name}`);
|
|
40
|
+
}
|
|
41
|
+
const executionSteps = [];
|
|
42
|
+
for (const step of workflow.dbSteps) {
|
|
43
|
+
const registeredStep = workflow.steps.find((workflowStep) => {
|
|
44
|
+
return workflowStep.name === step.name;
|
|
45
|
+
});
|
|
46
|
+
if (!registeredStep) {
|
|
47
|
+
throw new Error(`workflow step not found in registry: ${step.name}`);
|
|
48
|
+
}
|
|
49
|
+
executionSteps.push({
|
|
50
|
+
workflowStepId: step.id,
|
|
51
|
+
stepIndex: step.stepIndex,
|
|
52
|
+
stepName: step.name,
|
|
53
|
+
maxRetries: step.maxRetries,
|
|
54
|
+
input: resolveStepInput(registeredStep, input),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const execution = await insertWorkflowExecution(pool, {
|
|
58
|
+
workflowId: workflow.dbWorkflow.id,
|
|
59
|
+
workflowName: workflow.dbWorkflow.name,
|
|
60
|
+
workflowVersion: workflow.dbWorkflow.version,
|
|
61
|
+
input,
|
|
62
|
+
steps: executionSteps,
|
|
63
|
+
});
|
|
64
|
+
if (workflowQueue === null) {
|
|
65
|
+
console.warn(`[Flowwatch] ⚠️ Workflow "${name}" execution recorded but NOT queued — Redis queue unavailable. Upgrade to Redis ≥ 5 to enable background execution.`);
|
|
66
|
+
return { executionId: execution.executionId };
|
|
67
|
+
}
|
|
68
|
+
await addWorkflowJobToQueue(workflowQueue, execution.executionId);
|
|
69
|
+
return execution;
|
|
70
|
+
}, {
|
|
71
|
+
workflowName: name,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
workflow,
|
|
76
|
+
trigger,
|
|
77
|
+
getWorkflow
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function resolveStepInput(step, workflowInput) {
|
|
81
|
+
if (typeof step.input === "function") {
|
|
82
|
+
return step.input(workflowInput);
|
|
83
|
+
}
|
|
84
|
+
if (step.input !== undefined) {
|
|
85
|
+
return step.input;
|
|
86
|
+
}
|
|
87
|
+
return workflowInput;
|
|
88
|
+
}
|
|
89
|
+
function validateWorkflow(name, steps) {
|
|
90
|
+
if (name.trim().length === 0) {
|
|
91
|
+
throw new Error("workflow name must be a non-empty string");
|
|
92
|
+
}
|
|
93
|
+
if (steps.length === 0) {
|
|
94
|
+
throw new Error("workflow steps must not be empty");
|
|
95
|
+
}
|
|
96
|
+
const stepNames = new Set();
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
if (step.name.trim().length === 0) {
|
|
99
|
+
throw new Error("workflow step name must be a non-empty string");
|
|
100
|
+
}
|
|
101
|
+
if (typeof step.run !== "function") {
|
|
102
|
+
throw new Error(`workflow step ${step.name} run must be a function`);
|
|
103
|
+
}
|
|
104
|
+
if (step.retries !== undefined && (!Number.isInteger(step.retries) || step.retries < 0)) {
|
|
105
|
+
throw new Error(`workflow step ${step.name} retries must be a non-negative integer`);
|
|
106
|
+
}
|
|
107
|
+
if (stepNames.has(step.name)) {
|
|
108
|
+
throw new Error(`workflow step name must be unique: ${step.name}`);
|
|
109
|
+
}
|
|
110
|
+
stepNames.add(step.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createFlowwatch } from "./createFlowwatch.js";
|
|
2
|
+
export type { Flowwatch } from "./createFlowwatch.js";
|
|
3
|
+
export type { FlowwatchConfig } from "./types/index.js";
|
|
4
|
+
export type { TriggerWorkflow, WorkflowStep, WorkflowStepHandler } from "./engine/workflows/types.js";
|
|
5
|
+
export type { EvaluateFlag, FlagContext } from "./engine/flags/types.js";
|
|
6
|
+
export type { ActiveTraceSpan, TraceFunction } from "./engine/trace/traceEngine.js";
|
|
7
|
+
export type { TraceSpanType, TraceStatus } from "./persistence/repositories/traces/traceRepository.js";
|
|
8
|
+
export { getCurrentClientIp, getCurrentSpanId, getCurrentTraceId } from "./runtime/tracing/traceContext.js";
|
|
9
|
+
export { createRequestTracingMiddleware } from "./runtime/tracing/tracingMiddleware.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { withTransaction } from "../transaction.js";
|
|
2
|
+
export async function runMigrations(pool, migrationsToRun) {
|
|
3
|
+
await createMigrationsTable(pool);
|
|
4
|
+
await withMigrationLock(pool, async () => {
|
|
5
|
+
const appliedMigrations = await getAppliedMigrations(pool);
|
|
6
|
+
for (const migration of migrationsToRun) {
|
|
7
|
+
if (appliedMigrations.has(migration.name)) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
await withTransaction(pool, async (client) => {
|
|
11
|
+
await client.query(migration.up);
|
|
12
|
+
await client.query("INSERT INTO flowwatch_migrations (name) VALUES ($1)", [migration.name]);
|
|
13
|
+
});
|
|
14
|
+
appliedMigrations.add(migration.name);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function withMigrationLock(pool, fn) {
|
|
19
|
+
const client = await pool.connect();
|
|
20
|
+
try {
|
|
21
|
+
await client.query("SELECT pg_advisory_lock(hashtext('flowwatch:migrations'))");
|
|
22
|
+
return await fn();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await client.query("SELECT pg_advisory_unlock(hashtext('flowwatch:migrations'))");
|
|
26
|
+
client.release();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function createMigrationsTable(pool) {
|
|
30
|
+
await pool.query(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS flowwatch_migrations (
|
|
32
|
+
id SERIAL PRIMARY KEY,
|
|
33
|
+
name TEXT NOT NULL UNIQUE,
|
|
34
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
async function getAppliedMigrations(pool) {
|
|
39
|
+
const result = await pool.query("SELECT name FROM flowwatch_migrations");
|
|
40
|
+
const migrationNames = [];
|
|
41
|
+
for (const row of result.rows) {
|
|
42
|
+
migrationNames.push(row.name);
|
|
43
|
+
}
|
|
44
|
+
const appliedMigrations = new Set(migrationNames);
|
|
45
|
+
return appliedMigrations;
|
|
46
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const migrations = [
|
|
2
|
+
{
|
|
3
|
+
name: "001_create_workflow_tables",
|
|
4
|
+
up: `CREATE TABLE IF NOT EXISTS flowwatch_workflows (
|
|
5
|
+
id UUID PRIMARY KEY,
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
8
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
9
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
10
|
+
|
|
11
|
+
CONSTRAINT flowwatch_workflows_name_version_unique UNIQUE (name, version)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS flowwatch_workflow_steps (
|
|
15
|
+
id UUID PRIMARY KEY,
|
|
16
|
+
workflow_id UUID NOT NULL REFERENCES flowwatch_workflows(id) ON DELETE CASCADE,
|
|
17
|
+
step_index INTEGER NOT NULL,
|
|
18
|
+
name TEXT NOT NULL,
|
|
19
|
+
max_retries INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
21
|
+
|
|
22
|
+
CONSTRAINT flowwatch_workflow_steps_workflow_step_index_unique UNIQUE (workflow_id, step_index),
|
|
23
|
+
CONSTRAINT flowwatch_workflow_steps_workflow_name_unique UNIQUE (workflow_id, name)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS flowwatch_workflow_executions (
|
|
27
|
+
id UUID PRIMARY KEY,
|
|
28
|
+
workflow_id UUID NOT NULL REFERENCES flowwatch_workflows(id),
|
|
29
|
+
workflow_name TEXT NOT NULL,
|
|
30
|
+
workflow_version INTEGER NOT NULL,
|
|
31
|
+
status TEXT NOT NULL,
|
|
32
|
+
input JSONB,
|
|
33
|
+
output JSONB,
|
|
34
|
+
error JSONB,
|
|
35
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
36
|
+
started_at TIMESTAMPTZ,
|
|
37
|
+
completed_at TIMESTAMPTZ,
|
|
38
|
+
failed_at TIMESTAMPTZ
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS flowwatch_workflow_step_executions (
|
|
42
|
+
id UUID PRIMARY KEY,
|
|
43
|
+
execution_id UUID NOT NULL REFERENCES flowwatch_workflow_executions(id) ON DELETE CASCADE,
|
|
44
|
+
workflow_step_id UUID REFERENCES flowwatch_workflow_steps(id),
|
|
45
|
+
step_index INTEGER NOT NULL,
|
|
46
|
+
step_name TEXT NOT NULL,
|
|
47
|
+
status TEXT NOT NULL,
|
|
48
|
+
input JSONB,
|
|
49
|
+
output JSONB,
|
|
50
|
+
error JSONB,
|
|
51
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
max_retries INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
54
|
+
started_at TIMESTAMPTZ,
|
|
55
|
+
completed_at TIMESTAMPTZ,
|
|
56
|
+
failed_at TIMESTAMPTZ,
|
|
57
|
+
next_retry_at TIMESTAMPTZ,
|
|
58
|
+
|
|
59
|
+
CONSTRAINT flowwatch_workflow_step_executions_execution_step_index_unique UNIQUE (execution_id, step_index)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS flowwatch_workflow_executions_workflow_status_idx
|
|
63
|
+
ON flowwatch_workflow_executions (workflow_name, status);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS flowwatch_workflow_executions_created_at_idx
|
|
66
|
+
ON flowwatch_workflow_executions (created_at DESC);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS flowwatch_workflow_step_executions_execution_idx
|
|
69
|
+
ON flowwatch_workflow_step_executions (execution_id, step_index);
|
|
70
|
+
|
|
71
|
+
CREATE INDEX IF NOT EXISTS flowwatch_workflow_step_executions_status_retry_idx
|
|
72
|
+
ON flowwatch_workflow_step_executions (status, next_retry_at);`
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "002_create_feature_flag_tables",
|
|
76
|
+
up: `CREATE TABLE IF NOT EXISTS flowwatch_feature_flags (
|
|
77
|
+
id UUID PRIMARY KEY,
|
|
78
|
+
key TEXT NOT NULL UNIQUE,
|
|
79
|
+
description TEXT,
|
|
80
|
+
enabled BOOLEAN NOT NULL DEFAULT false,
|
|
81
|
+
rollout_percentage INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
83
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
84
|
+
|
|
85
|
+
CONSTRAINT flowwatch_feature_flags_rollout_percentage_check
|
|
86
|
+
CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS flowwatch_feature_flag_rules (
|
|
90
|
+
id UUID PRIMARY KEY,
|
|
91
|
+
flag_id UUID NOT NULL REFERENCES flowwatch_feature_flags(id) ON DELETE CASCADE,
|
|
92
|
+
attribute TEXT NOT NULL,
|
|
93
|
+
operator TEXT NOT NULL,
|
|
94
|
+
value JSONB NOT NULL,
|
|
95
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
96
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
97
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS flowwatch_feature_flag_audit_logs (
|
|
101
|
+
id UUID PRIMARY KEY,
|
|
102
|
+
flag_id UUID REFERENCES flowwatch_feature_flags(id) ON DELETE SET NULL,
|
|
103
|
+
action TEXT NOT NULL,
|
|
104
|
+
before JSONB,
|
|
105
|
+
after JSONB,
|
|
106
|
+
changed_by TEXT,
|
|
107
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS flowwatch_feature_flag_rules_flag_id_idx
|
|
111
|
+
ON flowwatch_feature_flag_rules (flag_id);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS flowwatch_feature_flag_audit_logs_flag_id_created_at_idx
|
|
114
|
+
ON flowwatch_feature_flag_audit_logs (flag_id, created_at DESC);`
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "003_create_error_capture_tables",
|
|
118
|
+
up: `CREATE TABLE IF NOT EXISTS flowwatch_request_traces (
|
|
119
|
+
id UUID PRIMARY KEY,
|
|
120
|
+
method TEXT NOT NULL,
|
|
121
|
+
path TEXT NOT NULL,
|
|
122
|
+
status_code INTEGER,
|
|
123
|
+
duration_ms INTEGER,
|
|
124
|
+
user_id TEXT,
|
|
125
|
+
ip TEXT,
|
|
126
|
+
user_agent TEXT,
|
|
127
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
128
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
129
|
+
ended_at TIMESTAMPTZ,
|
|
130
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
CREATE TABLE IF NOT EXISTS flowwatch_trace_spans (
|
|
134
|
+
id UUID PRIMARY KEY,
|
|
135
|
+
trace_id UUID NOT NULL REFERENCES flowwatch_request_traces(id) ON DELETE CASCADE,
|
|
136
|
+
parent_span_id UUID REFERENCES flowwatch_trace_spans(id) ON DELETE SET NULL,
|
|
137
|
+
name TEXT NOT NULL,
|
|
138
|
+
type TEXT NOT NULL,
|
|
139
|
+
status TEXT NOT NULL,
|
|
140
|
+
duration_ms INTEGER,
|
|
141
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
142
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
143
|
+
ended_at TIMESTAMPTZ,
|
|
144
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS flowwatch_errors (
|
|
148
|
+
id UUID PRIMARY KEY,
|
|
149
|
+
trace_id UUID REFERENCES flowwatch_request_traces(id) ON DELETE SET NULL,
|
|
150
|
+
span_id UUID REFERENCES flowwatch_trace_spans(id) ON DELETE SET NULL,
|
|
151
|
+
source TEXT NOT NULL,
|
|
152
|
+
category TEXT NOT NULL,
|
|
153
|
+
level TEXT NOT NULL DEFAULT 'error',
|
|
154
|
+
message TEXT NOT NULL,
|
|
155
|
+
stack TEXT,
|
|
156
|
+
name TEXT,
|
|
157
|
+
status_code INTEGER,
|
|
158
|
+
fingerprint TEXT,
|
|
159
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
160
|
+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
161
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE INDEX IF NOT EXISTS flowwatch_request_traces_started_at_idx
|
|
165
|
+
ON flowwatch_request_traces (started_at DESC);
|
|
166
|
+
|
|
167
|
+
CREATE INDEX IF NOT EXISTS flowwatch_request_traces_path_status_idx
|
|
168
|
+
ON flowwatch_request_traces (path, status_code);
|
|
169
|
+
|
|
170
|
+
CREATE INDEX IF NOT EXISTS flowwatch_trace_spans_trace_started_at_idx
|
|
171
|
+
ON flowwatch_trace_spans (trace_id, started_at ASC);
|
|
172
|
+
|
|
173
|
+
CREATE INDEX IF NOT EXISTS flowwatch_trace_spans_parent_span_id_idx
|
|
174
|
+
ON flowwatch_trace_spans (parent_span_id);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX IF NOT EXISTS flowwatch_errors_trace_id_idx
|
|
177
|
+
ON flowwatch_errors (trace_id);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS flowwatch_errors_span_id_idx
|
|
180
|
+
ON flowwatch_errors (span_id);
|
|
181
|
+
|
|
182
|
+
CREATE INDEX IF NOT EXISTS flowwatch_errors_source_category_idx
|
|
183
|
+
ON flowwatch_errors (source, category);
|
|
184
|
+
|
|
185
|
+
CREATE INDEX IF NOT EXISTS flowwatch_errors_occurred_at_idx
|
|
186
|
+
ON flowwatch_errors (occurred_at DESC);
|
|
187
|
+
|
|
188
|
+
CREATE INDEX IF NOT EXISTS flowwatch_errors_fingerprint_idx
|
|
189
|
+
ON flowwatch_errors (fingerprint);`
|
|
190
|
+
}
|
|
191
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
export type ErrorSource = "http" | "workflow" | "feature_flag" | "background_worker" | "dashboard_api" | "unknown";
|
|
3
|
+
export type ErrorCategory = "client" | "server" | "dependency" | "database" | "configuration" | "unknown";
|
|
4
|
+
export type ErrorLevel = "warning" | "error" | "fatal";
|
|
5
|
+
export interface ErrorRow {
|
|
6
|
+
id: string;
|
|
7
|
+
trace_id: string | null;
|
|
8
|
+
span_id: string | null;
|
|
9
|
+
source: ErrorSource;
|
|
10
|
+
category: ErrorCategory;
|
|
11
|
+
level: ErrorLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
stack: string | null;
|
|
14
|
+
name: string | null;
|
|
15
|
+
status_code: number | null;
|
|
16
|
+
fingerprint: string | null;
|
|
17
|
+
metadata: unknown;
|
|
18
|
+
occurred_at: Date;
|
|
19
|
+
created_at: Date;
|
|
20
|
+
}
|
|
21
|
+
export interface CreateErrorInput {
|
|
22
|
+
traceId?: string;
|
|
23
|
+
spanId?: string;
|
|
24
|
+
source: ErrorSource;
|
|
25
|
+
category: ErrorCategory;
|
|
26
|
+
level?: ErrorLevel;
|
|
27
|
+
message: string;
|
|
28
|
+
stack?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
statusCode?: number;
|
|
31
|
+
fingerprint?: string;
|
|
32
|
+
metadata?: unknown;
|
|
33
|
+
occurredAt?: Date;
|
|
34
|
+
}
|
|
35
|
+
export declare function createError(pool: Pool, input: CreateErrorInput): Promise<ErrorRow>;
|
|
36
|
+
export declare function getErrorsByTrace(pool: Pool, traceId: string): Promise<ErrorRow[]>;
|
|
37
|
+
export declare function getErrorById(pool: Pool, errorId: string): Promise<ErrorRow | undefined>;
|
|
38
|
+
export declare function listErrors(pool: Pool, limit?: number): Promise<ErrorRow[]>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export async function createError(pool, input) {
|
|
3
|
+
const result = await pool.query(`
|
|
4
|
+
INSERT INTO flowwatch_errors (
|
|
5
|
+
id,
|
|
6
|
+
trace_id,
|
|
7
|
+
span_id,
|
|
8
|
+
source,
|
|
9
|
+
category,
|
|
10
|
+
level,
|
|
11
|
+
message,
|
|
12
|
+
stack,
|
|
13
|
+
name,
|
|
14
|
+
status_code,
|
|
15
|
+
fingerprint,
|
|
16
|
+
metadata,
|
|
17
|
+
occurred_at
|
|
18
|
+
)
|
|
19
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13)
|
|
20
|
+
RETURNING *
|
|
21
|
+
`, [
|
|
22
|
+
randomUUID(),
|
|
23
|
+
input.traceId ?? null,
|
|
24
|
+
input.spanId ?? null,
|
|
25
|
+
input.source,
|
|
26
|
+
input.category,
|
|
27
|
+
input.level ?? "error",
|
|
28
|
+
input.message,
|
|
29
|
+
input.stack ?? null,
|
|
30
|
+
input.name ?? null,
|
|
31
|
+
input.statusCode ?? null,
|
|
32
|
+
input.fingerprint ?? null,
|
|
33
|
+
JSON.stringify(input.metadata ?? {}),
|
|
34
|
+
input.occurredAt ?? new Date(),
|
|
35
|
+
]);
|
|
36
|
+
return result.rows[0];
|
|
37
|
+
}
|
|
38
|
+
export async function getErrorsByTrace(pool, traceId) {
|
|
39
|
+
const result = await pool.query(`
|
|
40
|
+
SELECT *
|
|
41
|
+
FROM flowwatch_errors
|
|
42
|
+
WHERE trace_id = $1
|
|
43
|
+
ORDER BY occurred_at DESC
|
|
44
|
+
`, [traceId]);
|
|
45
|
+
return result.rows;
|
|
46
|
+
}
|
|
47
|
+
export async function getErrorById(pool, errorId) {
|
|
48
|
+
const result = await pool.query(`
|
|
49
|
+
SELECT *
|
|
50
|
+
FROM flowwatch_errors
|
|
51
|
+
WHERE id = $1
|
|
52
|
+
`, [errorId]);
|
|
53
|
+
return result.rows[0];
|
|
54
|
+
}
|
|
55
|
+
export async function listErrors(pool, limit = 50) {
|
|
56
|
+
const result = await pool.query(`
|
|
57
|
+
SELECT *
|
|
58
|
+
FROM flowwatch_errors
|
|
59
|
+
ORDER BY occurred_at DESC
|
|
60
|
+
LIMIT $1
|
|
61
|
+
`, [limit]);
|
|
62
|
+
return result.rows;
|
|
63
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
export type FlagRuleOperator = "equals" | "not_equals" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | "greater_than" | "less_than";
|
|
3
|
+
export interface FeatureFlagRow {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
rollout_percentage: number;
|
|
9
|
+
created_at: Date;
|
|
10
|
+
updated_at: Date;
|
|
11
|
+
}
|
|
12
|
+
export interface FeatureFlagRuleRow {
|
|
13
|
+
id: string;
|
|
14
|
+
flag_id: string;
|
|
15
|
+
attribute: string;
|
|
16
|
+
operator: FlagRuleOperator;
|
|
17
|
+
value: unknown;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
created_at: Date;
|
|
20
|
+
updated_at: Date;
|
|
21
|
+
}
|
|
22
|
+
export interface FeatureFlagAuditLogRow {
|
|
23
|
+
id: string;
|
|
24
|
+
flag_id: string | null;
|
|
25
|
+
action: string;
|
|
26
|
+
before: unknown;
|
|
27
|
+
after: unknown;
|
|
28
|
+
changed_by: string | null;
|
|
29
|
+
created_at: Date;
|
|
30
|
+
}
|
|
31
|
+
export interface FeatureFlagWithRuleCountRow extends FeatureFlagRow {
|
|
32
|
+
rule_count: number;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateFlagInput {
|
|
35
|
+
key: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
rolloutPercentage?: number;
|
|
39
|
+
changedBy?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface UpdateFlagInput {
|
|
42
|
+
description?: string | null;
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
rolloutPercentage?: number;
|
|
45
|
+
changedBy?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface CreateFlagRuleInput {
|
|
48
|
+
flagKey: string;
|
|
49
|
+
attribute: string;
|
|
50
|
+
operator: FlagRuleOperator;
|
|
51
|
+
value: unknown;
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
changedBy?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface UpdateFlagRuleInput {
|
|
56
|
+
attribute?: string;
|
|
57
|
+
operator?: FlagRuleOperator;
|
|
58
|
+
value?: unknown;
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
changedBy?: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function createFlag(pool: Pool, input: CreateFlagInput): Promise<FeatureFlagRow>;
|
|
63
|
+
export declare function listFlags(pool: Pool): Promise<FeatureFlagRow[]>;
|
|
64
|
+
export declare function listFlagsWithRuleCounts(pool: Pool): Promise<FeatureFlagWithRuleCountRow[]>;
|
|
65
|
+
export declare function getFlagByKey(pool: Pool, key: string): Promise<FeatureFlagRow | undefined>;
|
|
66
|
+
export declare function updateFlag(pool: Pool, key: string, input: UpdateFlagInput): Promise<FeatureFlagRow | undefined>;
|
|
67
|
+
export declare function deleteFlag(pool: Pool, key: string, changedBy?: string): Promise<boolean>;
|
|
68
|
+
export declare function listFlagRules(pool: Pool, flagKey: string): Promise<FeatureFlagRuleRow[]>;
|
|
69
|
+
export declare function createFlagRule(pool: Pool, input: CreateFlagRuleInput): Promise<FeatureFlagRuleRow | undefined>;
|
|
70
|
+
export declare function updateFlagRule(pool: Pool, ruleId: string, input: UpdateFlagRuleInput): Promise<FeatureFlagRuleRow | undefined>;
|
|
71
|
+
export declare function deleteFlagRule(pool: Pool, ruleId: string, changedBy?: string): Promise<boolean>;
|
|
72
|
+
export declare function listFlagAuditLogs(pool: Pool, flagKey: string): Promise<FeatureFlagAuditLogRow[]>;
|