Skip to content

Create AWS Benchmark dashboard script#1976

Open
cezudas wants to merge 15 commits intomainfrom
cezudas/OPS-3621
Open

Create AWS Benchmark dashboard script#1976
cezudas wants to merge 15 commits intomainfrom
cezudas/OPS-3621

Conversation

@cezudas
Copy link
Contributor

@cezudas cezudas commented Feb 19, 2026

Closes OPS-3738.

I tested locally by calling createAwsBenchmarkDashboard() from packages/server/api/src/main.ts.
I made sure the creation is idempotent, the new analytics dashboard and the resources it depends on (dataset, charts) are recreated with every run.

@linear
Copy link

linear bot commented Feb 19, 2026

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charts are not created when this file is missing, even though the dashboard import API returns no error.
I am keeping it for now, mocking sqlalchemy_uri. The end goal should be dashboard import would work without this file.

@cezudas cezudas marked this pull request as ready for review February 19, 2026 21:13
Copilot AI review requested due to automatic review settings February 19, 2026 21:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces functionality to create and seed an AWS Benchmark dashboard in the analytics system. The dashboard visualizes cost optimization opportunities and efficiency metrics for AWS resources, aggregating data from opportunities and timeseries tables.

Changes:

  • Added AWS Benchmark dashboard asset files (YAML configurations for charts, datasets, databases, and dashboard layout)
  • Implemented TypeScript functions to dynamically create virtual datasets and import dashboard configurations
  • Extended database connection handling to return UUIDs alongside IDs for dashboard mapping
  • Added FormData upload support for dashboard ZIP imports

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
metadata.yaml Dashboard metadata configuration with version and timestamp
AWS_Benchmark_Opportunities.yaml Dataset definition filtering opportunities table for AWS Benchmark workflows
AWS_Benchmark_KPI_efficiency.yaml Virtual dataset computing efficiency KPI from opportunities and cost data
openops_tables_connection.yaml Database connection configuration (placeholder, replaced during import)
AWS_Benchmark.yaml Dashboard layout with 7 charts showing savings, opportunities, and breakdowns
Unified_Cost_Efficiency_Metric_8.yaml, Total_Opportunities_2.yaml, etc. Chart configurations for dashboard visualizations
create-database-connection.ts Enhanced to return database UUID for mapping during import
create-aws-benchmark-dashboard.ts Main orchestration function for dashboard creation
benchmark-dashboard-helpers.ts Utility functions for ZIP creation, import, and table resolution
requests-helpers.ts Added FormData POST support for dashboard import API

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 16 to 66
async function createAwsBenchmarkDatasets(
token: string,
databaseId: number,
opportunitiesTableName: string,
timeseriesTableName: string,
) {
const opportunities = await createVirtualDataset(token, {
tableName: 'AWS_Benchmark_Opportunities',
sql: `SELECT *
FROM public."${opportunitiesTableName}"
WHERE "Workflow" LIKE 'AWS Benchmark%'
`,
databaseId,
schema: 'public',
recreateIfExists: true,
});

const kpi = await createVirtualDataset(token, {
tableName: 'AWS_Benchmark_KPI_efficiency',
sql: `WITH opp AS (
SELECT
"Account" AS opp_account,
COALESCE(SUM("Estimated savings USD per month"), 0) AS opp_sum
FROM public."${opportunitiesTableName}"
WHERE "Workflow" LIKE 'AWS Benchmark%'
GROUP BY "Account"
),
ts AS (
SELECT
"Account" AS ts_account,
COALESCE(SUM("Value"), 0) AS ts_value
FROM public."${timeseriesTableName}"
WHERE "Workflow" = 'Run AWS Benchmark'
AND "Date" = (date_trunc('month', current_date) - interval '1 month')::date
GROUP BY "Account"
)
SELECT
COALESCE(opp.opp_account, ts.ts_account) AS "Account",
COALESCE(opp.opp_sum, 0) AS savings,
COALESCE(ts.ts_value, 0) AS monthly_cost,
(1 - (COALESCE(opp.opp_sum, 0) / NULLIF(COALESCE(ts.ts_value, 0), 0))) * 100.0 AS kpi
FROM opp
FULL OUTER JOIN ts
ON opp.opp_account = ts.ts_account;
`,
databaseId,
schema: 'public',
recreateIfExists: true,
});

return { opportunities, kpi };
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: If the dataset creation fails (either 'opportunities' or 'kpi' dataset), the function should handle the error gracefully. Currently, if createVirtualDataset throws an error, it will propagate up without any logging or cleanup. Consider wrapping the dataset creation calls in try-catch blocks to provide more informative error messages and ensure proper cleanup if needed.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createVirtualDataset errors bubble to Fastify's global handler → HTTP 500, which is correct, and createDataset already logs each success with table name and dataset ID, while makeOpenOpsAnalyticsV1ApiRequest logs the route on failure; there's no cleanup needed since recreateIfExists: true makes the next call idempotent anyway.

parents:
- ROOT_ID
- GRID_ID
- ROW-OryzygZMMHPbYac9DWioo
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent parent-child relationship: CHART-explore-8-1 declares 'ROW-OryzygZMMHPbYac9DWioo' as its parent on line 106, but ROW-ndno4IT57hnxkF0gN3Pp2 lists CHART-explore-8-1 as a child on line 179. Additionally, ROW-OryzygZMMHPbYac9DWioo is not defined anywhere in the dashboard configuration and is not listed in GRID_ID's children. This creates an inconsistent state. The parent reference should be updated to 'ROW-ndno4IT57hnxkF0gN3Pp2' to match the actual containment structure.

Suggested change
- ROW-OryzygZMMHPbYac9DWioo
- ROW-ndno4IT57hnxkF0gN3Pp2

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +21
export function getDashboardFolderPath(providerKey: string): string {
return path.join(
process.cwd(),
'packages',
'server',
'api',
'src',
'assets',
`${providerKey}-benchmark-dashboard`,
);
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded relative path may break in different execution contexts: The getDashboardFolderPath function uses process.cwd() to construct the path to assets. This assumes the current working directory is the repository root, which may not be true in all execution contexts (e.g., when running tests, in production environments, or when the script is invoked from a different directory). Consider using __dirname or import.meta.url to create a path relative to the current file, or use a configurable base path from system properties.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the two actual execution contexts (local dev + Docker), the code is correct and consistent with existing patterns.This pattern is also an established convention in the repo ( see https://github.com/openops-cloud/openops/blob/main/packages/server/api/src/app/ai/chat/prompts.service.ts )

}

const USERFRIENDLTABLE_SUFFIX = '_userfriendly';

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JSDoc documentation for public functions: The exported functions createAwsBenchmarkDashboard, createDashboardZipBuffer, importDashboardFromZip, and resolveTableIds lack documentation comments explaining their purpose, parameters, return values, and potential side effects. Given that these are public APIs that will be used by other parts of the codebase, they should have comprehensive JSDoc comments to improve maintainability and developer experience.

Suggested change
/**
* Seeds the AWS Benchmark dashboard for the oldest organization.
*
* This function:
* - Authenticates as the OpenOps Analytics admin.
* - Ensures a Postgres connection for the OpenOps tables database.
* - Resolves the default project and associated table IDs.
* - Creates the virtual datasets required by the AWS Benchmark dashboard.
* - Imports the AWS Benchmark dashboard definition using the created datasets.
*
* @returns A promise that resolves when the AWS Benchmark dashboard
* has been successfully created and imported.
*
* @throws If required system properties are missing, or if the
* organization or project cannot be resolved.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +37
function addFolderToZip(folderPath: string, zipFolder: JSZip): void {
const entries = fs.readdirSync(folderPath);
for (const entry of entries) {
const fullPath = path.join(folderPath, entry);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
const subFolder = zipFolder.folder(entry);
if (subFolder) {
addFolderToZip(fullPath, subFolder);
}
} else {
zipFolder.file(entry, fs.readFileSync(fullPath));
}
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for file system operations: The addFolderToZip function performs synchronous file system operations (fs.readdirSync, fs.statSync, fs.readFileSync) without error handling. If any of these operations fail (e.g., due to missing files, permission issues, or IO errors), the error will propagate without context. Consider wrapping these operations in try-catch blocks and providing more informative error messages that include the problematic file path.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@cezudas cezudas Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node's fs errors already include the offending path, and a missing asset directory is a deployment error that correctly propagates as an HTTP 500 through Fastify's global error handler — no try/catch needed.

Comment on lines 24 to 59
sql: `SELECT *
FROM public."${opportunitiesTableName}"
WHERE "Workflow" LIKE 'AWS Benchmark%'
`,
databaseId,
schema: 'public',
recreateIfExists: true,
});

const kpi = await createVirtualDataset(token, {
tableName: 'AWS_Benchmark_KPI_efficiency',
sql: `WITH opp AS (
SELECT
"Account" AS opp_account,
COALESCE(SUM("Estimated savings USD per month"), 0) AS opp_sum
FROM public."${opportunitiesTableName}"
WHERE "Workflow" LIKE 'AWS Benchmark%'
GROUP BY "Account"
),
ts AS (
SELECT
"Account" AS ts_account,
COALESCE(SUM("Value"), 0) AS ts_value
FROM public."${timeseriesTableName}"
WHERE "Workflow" = 'Run AWS Benchmark'
AND "Date" = (date_trunc('month', current_date) - interval '1 month')::date
GROUP BY "Account"
)
SELECT
COALESCE(opp.opp_account, ts.ts_account) AS "Account",
COALESCE(opp.opp_sum, 0) AS savings,
COALESCE(ts.ts_value, 0) AS monthly_cost,
(1 - (COALESCE(opp.opp_sum, 0) / NULLIF(COALESCE(ts.ts_value, 0), 0))) * 100.0 AS kpi
FROM opp
FULL OUTER JOIN ts
ON opp.opp_account = ts.ts_account;
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential SQL injection vulnerability: The table name variables 'opportunitiesTableName' and 'timeseriesTableName' are directly interpolated into SQL strings without proper sanitization or parameterization. While these values appear to come from internal sources (constructed from constants and table IDs), they should still be properly sanitized or validated to prevent SQL injection. Consider using parameterized queries or at least validating that these table names only contain safe characters (alphanumeric, underscores).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No user input touches this path at any point. The numeric table.id coming from the database is typed as number and cannot contain SQL metacharacters.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Comment on lines 79 to 88
const dbConnection = await getOrCreatePostgresDatabaseConnection(
access_token,
system.getOrThrow(AppSystemProp.OPENOPS_TABLES_DATABASE_NAME),
system.getOrThrow(AppSystemProp.POSTGRES_PASSWORD),
system.getOrThrow(AppSystemProp.POSTGRES_PORT),
system.getOrThrow(AppSystemProp.POSTGRES_USERNAME),
system.get(AppSystemProp.OPENOPS_TABLES_DB_HOST) ??
system.getOrThrow(AppSystemProp.POSTGRES_HOST),
'openops_tables_connection',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using this block in 2 different places. Please extract it to a method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return Object.fromEntries(entries);
} catch (error) {
logger.error('Could not find required tables', { tableNames, error });
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw the error here. There's no need to do it outside.

    throw new Error(
      'Could not resolve required table IDs for benchmark dashboard',
    );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 60 to 82
const formData = new FormData();

formData.append('formData', zipBuffer, {
filename: 'dashboard.zip',
contentType: 'application/zip',
});

formData.append('overwrite', 'true');

const databaseMapping = {
openops_tables_connection: databaseUuid,
};
formData.append('database_mapping', JSON.stringify(databaseMapping));
formData.append('dataset_mapping', JSON.stringify(datasetMapping));

logger.info('Sending dashboard import request', {
databaseMapping,
datasetMapping,
});

await makeOpenOpsAnalyticsPostFormData('dashboard/import/', token, formData);

logger.info('Successfully imported dashboard');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a try-catch block. This way, if something goes wrong, we'll have the error log in the right place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@sonarqubecloud
Copy link

return { opportunities, kpi };
}

const USERFRIENDLYTABLE_SUFFIX = '_userfriendly';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And move it to the top of the file

Suggested change
const USERFRIENDLYTABLE_SUFFIX = '_userfriendly';
const USER_FRIENDLY_TABLE_SUFFIX = '_userfriendly';

);
return Object.fromEntries(entries);
} catch (error) {
logger.error('Could not find required tables', { tableNames, error });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.error('Could not find required tables', { tableNames, error });
logger.error('Could not resolve required table IDs for benchmark dashboard', { tableNames, error });

Comment on lines +118 to +120
throw new Error(
'Could not resolve required table IDs for benchmark dashboard',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This preserves the original stack entirely.

Suggested change
throw new Error(
'Could not resolve required table IDs for benchmark dashboard',
);
throw error;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments