diff --git a/packages/README.md b/packages/README.md index 338c218..32fd972 100644 --- a/packages/README.md +++ b/packages/README.md @@ -1,6 +1,6 @@ # Contribution Guide -This project hosts customer-specific elements like dashboards and events for IBM Instana monitoring, bundled into integration packages. +This project hosts customer-specific elements like dashboards, events, entities, and smart alerts for IBM Instana monitoring, bundled into integration packages. ## Steps to Contribute @@ -11,47 +11,43 @@ Clone this repository and areate a dev branch based on `main`. git clone https://github.com/instana/observability-as-code.git ``` -### 2. **Create a new package:** -Under `packages/`, create a new directory (e.g. `@instana-integration/packagename`). It should contain: +### 2. **Initialize a new package:** +Use the `init` command to create a new package structure under `packages/@instana-integration/packagename`. The CLI will generate the `package.json` and `README.md` files and create directories for the integration element types you select. + +Supported integration element types: +- **Dashboards**: Custom monitoring dashboards +- **Events**: Custom event definitions +- **Entities**: Custom entity definitions +- **Smart Alerts**: Intelligent metric-based alerts + +Example package structure after initialization: ```shell packages/@instana-integration/packagename/ - ├── dashboards/ - │ └── my-dashboard.json - ├── events/ - │ └── my-event.json - └── package.json + ├── dashboards/ # (optional) Custom dashboards + ├── events/ # (optional) Event definitions + ├── entities/ # (optional) Entity definitions + ├── smart-alerts/ # (optional) Smart alert definitions + ├── README.md # Generated by init command + └── package.json # Generated by init command ``` -### 3. Build custom elements: +### 3. Build custom elements: * Define custom elements in the Instana UI - * Export custom elements from Instana UI to your local + * Export custom elements from Instana UI to your local package using the CLI * Set access rules to GLOBAL in dashboards for public sharing -### 4. Create package.json: -Create `package.json` file by using the `init` command provided by the Instana CLI for Integration Package Management and include required fields. - - ```shell - { - "name": "@instana-integration/package-name", - "version": "1.0.0", - "description": "Custom monitoring for XYZ systems.", - "author": "author", - "license": "MIT" - } - ``` - -### 5. Create a Pull Request: +### 4. Create a Pull Request: - Commit and push your changes - Open a PR against the `main` branch -### 6. Automated Publishing: +### 5. Automated Publishing: After your PR is reviewed and merged into main, the GitHub workflow will automatically publish your package to [Instana integration organization](https://www.npmjs.com/org/instana-integration). - ## 📚 Learn More - [Blog: Making Your Instana Dashboards Publicly Sharable](https://community.ibm.com/community/user/blogs/ying-mo2/2025/02/22/making-your-instana-dashboards-publicly-sharable) - [Blog: Contributing to IBM Instana Observability as Code GitHub Repository](https://community.ibm.com/community/user/blogs/ying-mo2/2025/03/09/contributing-to-ibm-instana-observability-as-code) - +- [Blog: Event Definitions in Instana: From Manual Setup to Reusable Packages](https://community.ibm.com/community/user/blogs/disha-bhagat/2025/08/20/sharing-and-reusing-instana-event-definitions) +- [Blog: Scale Your Alerting: Package and Share Instana Smart Alerts](https://community.ibm.com/community/user/blogs/disha-bhagat/2026/02/27/scale-your-alerting-package-and-share-smart-alerts) diff --git a/tools/integration/README.md b/tools/integration/README.md index c041801..d344ee6 100644 --- a/tools/integration/README.md +++ b/tools/integration/README.md @@ -2,6 +2,11 @@ The Instana CLI for Integration Package Management is used to manage the lifecycle of Instana integration package. For example, you can use this CLI to download the integration package from public website to your local machine, then install the package into an existing Instana environment. +## Requirements + +- **For binary users**: No Node.js installation required. The binary includes an embedded Node.js 18 runtime. +- **For developers**: Node.js 18 or higher is required when running from source code. + ## For end users ### Download the CLI diff --git a/tools/integration/src/__tests__/handlers/export.test.ts b/tools/integration/src/__tests__/handlers/export.test.ts index 9c44d0e..b4a02ed 100644 --- a/tools/integration/src/__tests__/handlers/export.test.ts +++ b/tools/integration/src/__tests__/handlers/export.test.ts @@ -592,11 +592,15 @@ describe('handleExport', () => { mockedUtils.sanitizeFileName = jest.fn().mockReturnValue('smart-alert-1'); mockFilterElementsBy.mockImplementation((items) => items); - // Mock the 3 smart-alert endpoints returning data from first endpoint + // Mock the 7 smart-alert endpoints returning data from first endpoint mockAxiosInstance.get .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) // mobile-app endpoint .mockResolvedValueOnce({ status: 200, data: [] }) // application endpoint .mockResolvedValueOnce({ status: 200, data: [] }) // infra endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // website endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // synthetics endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // service-levels endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // logs endpoint .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Smart Alert 1' } }); // fetch single alert await handleExport(argv); @@ -630,10 +634,14 @@ describe('handleExport', () => { mockedUtils.sanitizeFileName = jest.fn().mockReturnValue('smart-alert-1'); mockAxiosInstance.get - .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) - .mockResolvedValueOnce({ status: 200, data: [] }) - .mockResolvedValueOnce({ status: 200, data: [] }) - .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Smart Alert 1' } }); + .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) // mobile-app endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // application endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // infra endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // website endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // synthetics endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // service-levels endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // logs endpoint + .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Smart Alert 1' } }); // fetch single alert await handleExport(argv); @@ -705,6 +713,10 @@ describe('handleExport', () => { .mockResolvedValueOnce({ status: 200, data: mobileAlerts }) // mobile-app endpoint .mockResolvedValueOnce({ status: 200, data: appAlerts }) // application endpoint .mockResolvedValueOnce({ status: 200, data: infraAlerts }) // infra endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // website endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // synthetics endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // service-levels endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // logs endpoint .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Mobile Alert' } }) .mockResolvedValueOnce({ status: 200, data: { id: 'alert-2', name: 'App Alert' } }) .mockResolvedValueOnce({ status: 200, data: { id: 'alert-3', name: 'Infra Alert' } }); @@ -741,10 +753,14 @@ describe('handleExport', () => { ); mockAxiosInstance.get - .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) - .mockResolvedValueOnce({ status: 200, data: [] }) - .mockResolvedValueOnce({ status: 200, data: [] }) - .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Critical Alert' } }); + .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) // mobile-app endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // application endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // infra endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // website endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // synthetics endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // service-levels endpoint + .mockResolvedValueOnce({ status: 200, data: [] }) // logs endpoint + .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Critical Alert' } }); // fetch single alert await handleExport(argv); @@ -815,6 +831,10 @@ describe('handleExport', () => { .mockResolvedValueOnce({ status: 200, data: mobileAlerts }) // mobile-app endpoint succeeds .mockRejectedValueOnce(new Error('API Error')) // application endpoint fails .mockRejectedValueOnce(new Error('API Error')) // infra endpoint fails + .mockRejectedValueOnce(new Error('API Error')) // website endpoint fails + .mockRejectedValueOnce(new Error('API Error')) // synthetics endpoint fails + .mockRejectedValueOnce(new Error('API Error')) // service-levels endpoint fails + .mockRejectedValueOnce(new Error('API Error')) // logs endpoint fails .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Mobile Alert' } }); // fetch single alert await handleExport(argv); @@ -852,7 +872,15 @@ describe('handleExport', () => { .mockResolvedValueOnce({ status: 200, data: mockSmartAlerts }) // mobile-app list .mockResolvedValueOnce({ status: 200, data: [] }) // application list .mockResolvedValueOnce({ status: 200, data: [] }) // infra list - // First two endpoints fail, third succeeds + .mockResolvedValueOnce({ status: 200, data: [] }) // website list + .mockResolvedValueOnce({ status: 200, data: [] }) // synthetics list + .mockResolvedValueOnce({ status: 200, data: [] }) // service-levels list + .mockResolvedValueOnce({ status: 200, data: [] }) // logs list + // First six endpoints fail, seventh succeeds + .mockRejectedValueOnce(new Error('Not found')) + .mockRejectedValueOnce(new Error('Not found')) + .mockRejectedValueOnce(new Error('Not found')) + .mockRejectedValueOnce(new Error('Not found')) .mockRejectedValueOnce(new Error('Not found')) .mockRejectedValueOnce(new Error('Not found')) .mockResolvedValueOnce({ status: 200, data: { id: 'alert-1', name: 'Smart Alert 1' } }); @@ -886,8 +914,8 @@ describe('handleExport', () => { await handleExport(argv); - // Should call get for dashboards, events, entities, and smart-alerts (3 endpoints for smart-alerts) - expect(mockAxiosInstance.get).toHaveBeenCalledTimes(6); + // Should call get for dashboards, events, entities, and smart-alerts (7 endpoints for smart-alerts) + expect(mockAxiosInstance.get).toHaveBeenCalledTimes(10); expect(mockedLogger.info).toHaveBeenCalledWith('Total dashboard(s) processed: 0'); expect(mockedLogger.info).toHaveBeenCalledWith('Total event(s) processed: 0'); expect(mockedLogger.info).toHaveBeenCalledWith('Total entities processed: 0'); diff --git a/tools/integration/src/__tests__/handlers/import.test.ts b/tools/integration/src/__tests__/handlers/import.test.ts index 5cf2fb5..d36dd9d 100644 --- a/tools/integration/src/__tests__/handlers/import.test.ts +++ b/tools/integration/src/__tests__/handlers/import.test.ts @@ -307,12 +307,16 @@ describe('handleImport', () => { name: 'Test Infrastructure Smart Alert Multi', rules: [ { - entityType: 'kubernetesCluster', - metricName: 'test.metric1' + rule: { + entityType: 'kubernetesCluster', + metricName: 'test.metric1' + } }, { - entityType: 'host', - metricName: 'test.metric2' + rule: { + entityType: 'host', + metricName: 'test.metric2' + } } ], threshold: { value: 10 }, @@ -333,6 +337,156 @@ describe('handleImport', () => { expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully imported: 1')); }); + it('should import website smart alerts successfully', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + include: 'smart-alerts/**/*.json', + debug: false + }; + + mockedFs.existsSync = jest.fn().mockReturnValue(true); + mockedGlobSync.mockReturnValue(['/test/package/smart-alerts/website-alert.json']); + mockedFs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({ + name: 'Test Website Smart Alert', + websiteId: 'website-123', + rule: { + metricName: 'test.metric' + }, + threshold: { value: 10 }, + granularity: 60000, + timeThreshold: { type: 'violationsInSequence' } + })); + + mockAxiosInstance.post.mockResolvedValue({ status: 200 }); + + await handleImport(argv); + + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Detected website smart alert')); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + 'https://test-server.com/api/events/settings/website-alert-configs', + expect.objectContaining({ name: 'Test Website Smart Alert' }), + expect.any(Object) + ); + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully imported: 1')); + }); + + it('should import synthetics smart alerts successfully', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + include: 'smart-alerts/**/*.json', + debug: false + }; + + mockedFs.existsSync = jest.fn().mockReturnValue(true); + mockedGlobSync.mockReturnValue(['/test/package/smart-alerts/synthetics-alert.json']); + mockedFs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({ + name: 'Test Synthetics Smart Alert', + syntheticTestIds: ['test-123', 'test-456'], + rule: { + metricName: 'test.metric' + }, + threshold: { value: 10 }, + granularity: 60000, + timeThreshold: { type: 'violationsInSequence' } + })); + + mockAxiosInstance.post.mockResolvedValue({ status: 200 }); + + await handleImport(argv); + + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Detected synthetics smart alert')); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + 'https://test-server.com/api/events/settings/global-alert-configs/synthetics', + expect.objectContaining({ name: 'Test Synthetics Smart Alert' }), + expect.any(Object) + ); + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully imported: 1')); + }); + + it('should import service level smart alerts successfully', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + include: 'smart-alerts/**/*.json', + debug: false + }; + + mockedFs.existsSync = jest.fn().mockReturnValue(true); + mockedGlobSync.mockReturnValue(['/test/package/smart-alerts/slo-alert.json']); + mockedFs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({ + name: 'Test Service Level Smart Alert', + sloIds: ['slo-123', 'slo-456'], + rule: { + metricName: 'test.metric' + }, + threshold: { value: 10 }, + granularity: 60000, + timeThreshold: { type: 'violationsInSequence' } + })); + + mockAxiosInstance.post.mockResolvedValue({ status: 200 }); + + await handleImport(argv); + + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Detected service levels smart alert')); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + 'https://test-server.com/api/events/settings/global-alert-configs/service-levels', + expect.objectContaining({ name: 'Test Service Level Smart Alert' }), + expect.any(Object) + ); + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully imported: 1')); + }); + + it('should import logs smart alerts successfully', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + include: 'smart-alerts/**/*.json', + debug: false + }; + + mockedFs.existsSync = jest.fn().mockReturnValue(true); + mockedGlobSync.mockReturnValue(['/test/package/smart-alerts/logs-alert.json']); + mockedFs.readFileSync = jest.fn().mockReturnValue(JSON.stringify({ + name: 'Test Logs Smart Alert', + rules: [ + { + rule: { + aggregation: 'P99', + alertType: 'threshold', + metricName: 'test.metric' + }, + thresholdOperator: '<', + thresholds: { value: 10 } + } + ], + granularity: 60000, + timeThreshold: { type: 'violationsInSequence' } + })); + + mockAxiosInstance.post.mockResolvedValue({ status: 200 }); + + await handleImport(argv); + + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Detected logs smart alert')); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + 'https://test-server.com/api/events/settings/global-alert-configs/logs', + expect.objectContaining({ name: 'Test Logs Smart Alert' }), + expect.any(Object) + ); + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully imported: 1')); + }); + it('should handle smart alert type detection failure', async () => { const argv = { package: '/test/package', @@ -419,6 +573,31 @@ describe('handleImport', () => { // Should try to find in node_modules expect(mockedFs.existsSync).toHaveBeenCalledWith('@scope/package-name'); }); + + it('should handle missing entities folder gracefully when importing all', async () => { + const argv = { + package: '/test/package', + server: 'test-server.com', + token: 'test-token', + location: '/test/location', + debug: false + }; + + mockedFs.existsSync = jest.fn() + .mockReturnValueOnce(true) // package path exists + .mockReturnValueOnce(false); // entities path does NOT exist + mockedGlobSync.mockReturnValue([]); + mockedValidators.getEntityDashboardRefs = jest.fn().mockReturnValue(new Set()); + + await handleImport(argv); + + // Should log warning about missing entities folder + expect(mockedLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("No 'entities' folder found") + ); + // Should NOT crash and complete successfully + expect(mockedGlobSync).toHaveBeenCalled(); + }); }); describe('Parameter Substitution', () => { diff --git a/tools/integration/src/__tests__/validators.test.ts b/tools/integration/src/__tests__/validators.test.ts index 9168e7d..f35a209 100644 --- a/tools/integration/src/__tests__/validators.test.ts +++ b/tools/integration/src/__tests__/validators.test.ts @@ -597,6 +597,7 @@ describe('validators', () => { describe('getEntityDashboardRefs', () => { beforeEach(() => { + mockedFs.existsSync.mockReturnValue(true); // Default: directory exists mockedFs.readdirSync.mockReturnValue(['entity1.json'] as any); mockedFs.statSync.mockReturnValue({ isDirectory: () => false } as any); }); @@ -642,6 +643,15 @@ describe('validators', () => { expect(result.size).toBe(0); }); + it('should return empty set when entities directory does not exist', () => { + mockedFs.existsSync.mockReturnValue(false); + + const result = validators.getEntityDashboardRefs('/test/entities'); + + expect(result.size).toBe(0); + expect(mockedFs.readdirSync).not.toHaveBeenCalled(); + }); + it('should silently ignore parse errors', () => { mockedFs.readFileSync.mockReturnValue('invalid json'); diff --git a/tools/integration/src/handlers/export.ts b/tools/integration/src/handlers/export.ts index 3339326..06a213f 100644 --- a/tools/integration/src/handlers/export.ts +++ b/tools/integration/src/handlers/export.ts @@ -62,10 +62,6 @@ export async function handleExport(argv: any) { const eventsPath = path.join(location, "events"); const entitiesPath = path.join(location, "entities"); const smartAlertsPath = path.join(location, "smart-alerts"); - fs.mkdirSync(dashboardsPath, { recursive: true }); - fs.mkdirSync(eventsPath, { recursive: true }); - fs.mkdirSync(entitiesPath, {recursive: true}); - fs.mkdirSync(smartAlertsPath, {recursive: true}); let wasDashboardFound = false; let wasEventFound = false; @@ -74,6 +70,8 @@ export async function handleExport(argv: any) { // Dashboard export if (parsedIncludes.some(inc => inc.type === "dashboard" || inc.type === "all")) { + // Create dashboards folder only when exporting dashboards + fs.mkdirSync(dashboardsPath, { recursive: true }); const allDashboards = await fetchDashboards(server, token, axiosInstance); let totalDashboardProcessed = 0; @@ -118,6 +116,8 @@ export async function handleExport(argv: any) { // Event export if (parsedIncludes.some(inc => inc.type === "event" || inc.type === "all")) { + // Create events folder only when exporting events + fs.mkdirSync(eventsPath, { recursive: true }); const allEvents = await fetchEvents(server, token, axiosInstance); let totalEventProcessed = 0; @@ -162,6 +162,8 @@ export async function handleExport(argv: any) { // Entity export if (parsedIncludes.some(inc => inc.type === "entity" || inc.type === "all")){ + // Create entities folder only when exporting entities + fs.mkdirSync(entitiesPath, { recursive: true }); const allEntities = await fetchEntities(server, token, axiosInstance); let totalEntitiesProcessed = 0; @@ -211,6 +213,8 @@ export async function handleExport(argv: any) { // smart-alert export if (parsedIncludes.some(inc => inc.type === "smart-alert" || inc.type === "all")) { + // Create smart-alerts folder only when exporting smart-alerts + fs.mkdirSync(smartAlertsPath, { recursive: true }); const allSmartAlerts = await fetchSmartAlerts(server, token, axiosInstance); let totalSmartAlertsProcessed = 0; @@ -265,7 +269,11 @@ async function fetchSmartAlerts(server: string, token: string, axiosInstance: an const urls = [ `https://${server}/api/events/settings/mobile-app-alert-configs`, `https://${server}/api/events/settings/application-alert-configs`, - `https://${server}/api/events/settings/infra-alert-configs` + `https://${server}/api/events/settings/infra-alert-configs`, + `https://${server}/api/events/settings/website-alert-configs`, + `https://${server}/api/events/settings/global-alert-configs/synthetics`, + `https://${server}/api/events/settings/global-alert-configs/service-levels`, + `https://${server}/api/events/settings/global-alert-configs/logs` ]; if (alertId) { @@ -378,6 +386,12 @@ function saveEntity(entityDir: string, dashboardDir: string, entity: any) { const dashboards = entity.data?.dashboards || []; const updatedDashboards: any[] = []; + // Create dashboards folder if entity has embedded dashboards + if (dashboards.length > 0 && !fs.existsSync(dashboardDir)) { + fs.mkdirSync(dashboardDir, { recursive: true }); + logger.debug('Created dashboards folder for entity embedded dashboards'); + } + dashboards.forEach((dashboard: any, index: number) => { const dashboardContent = dashboard; const dashboardFileName = `${entityName}_dashboard_${index + 1}.json`; diff --git a/tools/integration/src/handlers/import.ts b/tools/integration/src/handlers/import.ts index 1a76764..b2a7a44 100644 --- a/tools/integration/src/handlers/import.ts +++ b/tools/integration/src/handlers/import.ts @@ -101,11 +101,19 @@ export async function handleImport(argv: any) { try { actualApiPath = determineSmartAlertAPI(jsonContent); - const typeName = actualApiPath.includes('infra') - ? 'infrastructure' + const typeName = actualApiPath.includes('mobile-app') + ? 'mobile app' : actualApiPath.includes('application') ? 'application' - : 'mobile app'; + : actualApiPath.includes('website') + ? 'website' + : actualApiPath.includes('synthetics') + ? 'synthetics' + : actualApiPath.includes('service-levels') + ? 'service levels' + : actualApiPath.includes('infra') + ? 'infrastructure' + : 'logs'; logger.info(`Detected ${typeName} smart alert`); } catch (err) { @@ -258,18 +266,26 @@ export async function handleImport(argv: any) { } } else { const entitiesPath = path.join(packagePath, 'entities'); - const referencedDashboards = validators.getEntityDashboardRefs(entitiesPath); - const referencedDashboardPaths = new Set(); - - const allDashboardFiles = globSync(path.join(packagePath, 'dashboards', '**/*.json')); - allDashboardFiles.forEach(file => { - const filename = path.basename(file); - if (referencedDashboards.has(filename)) { - referencedDashboardPaths.add(file); - } - }); + let referencedDashboards = new Set(); + + // Only check for entity dashboard references if entities folder exists + if (fs.existsSync(entitiesPath)) { + referencedDashboards = validators.getEntityDashboardRefs(entitiesPath); + } else { + logger.warn(`No 'entities' folder found — cannot check for entity dashboards.`); + } + + const referencedDashboardPaths = new Set(); + + const allDashboardFiles = globSync(path.join(packagePath, 'dashboards', '**/*.json')); + allDashboardFiles.forEach(file => { + const filename = path.basename(file); + if (referencedDashboards.has(filename)) { + referencedDashboardPaths.add(file); + } + }); - await importIntegration(path.join(packagePath, 'dashboards', '**/*.json'), "api/custom-dashboard", "dashboard", referencedDashboardPaths); + await importIntegration(path.join(packagePath, 'dashboards', '**/*.json'), "api/custom-dashboard", "dashboard", referencedDashboardPaths); for (const defaultFolder of defaultEventsFolders) { const searchPattern = path.join(packagePath, defaultFolder, '**/*.json'); await importIntegration(searchPattern, "api/events/settings/event-specifications/custom", "event"); @@ -519,14 +535,30 @@ function determineSmartAlertAPI(alertJson: any): string { else if (alertJson.applicationId || alertJson.applications) { return 'api/events/settings/application-alert-configs'; } - // Check for infrastructure alert - else if (alertJson.rule?.entityType || (Array.isArray(alertJson.rules) && alertJson.rules.length > 0)) { + // Check for website alert + else if (alertJson.websiteId) { + return 'api/events/settings/website-alert-configs'; + } + // Check for synthetics alert + else if (alertJson.syntheticTestIds) { + return 'api/events/settings/global-alert-configs/synthetics'; + } + // Check for service level alert + else if (alertJson.sloIds) { + return 'api/events/settings/global-alert-configs/service-levels'; + } + // Check for infrastructure alert (has rule.entityType OR rules array with rule.entityType in items) + else if (alertJson.rule?.entityType || (Array.isArray(alertJson.rules) && alertJson.rules.some((r: any) => r.rule?.entityType))) { return 'api/events/settings/infra-alert-configs'; } + // Check for logs alert (has rules array - fallback for remaining alerts with rules) + else if (alertJson.rules) { + return 'api/events/settings/global-alert-configs/logs'; + } // Unknown smart alert type else { - throw new Error( - `Alert must have one of: mobileAppId, applicationId/applications, or rule.entityType` - ); + throw new Error( + `Unable to determine smart alert type. Alert must have one of: mobileAppId, applicationId/applications, websiteId, syntheticTestIds, sloIds, rule.entityType, or rules array` + ); } } \ No newline at end of file diff --git a/tools/integration/src/validators.ts b/tools/integration/src/validators.ts index 3f9f916..472cbb0 100644 --- a/tools/integration/src/validators.ts +++ b/tools/integration/src/validators.ts @@ -305,6 +305,12 @@ export function validateEntityFiles( */ export function getEntityDashboardRefs(entitiesPath: string): Set { const embeddedDashboardRefs = new Set(); + + // Return empty set if entities path doesn't exist + if (!fs.existsSync(entitiesPath)) { + return embeddedDashboardRefs; + } + const jsonFiles = getAllJsonFiles(entitiesPath); jsonFiles.forEach(filePath => {