Skip to content

feat: Add New Relic integration#3322

Open
vukasin-djuricic wants to merge 13 commits intosuperplanehq:mainfrom
vukasin-djuricic:feat/add-newrelic-integration
Open

feat: Add New Relic integration#3322
vukasin-djuricic wants to merge 13 commits intosuperplanehq:mainfrom
vukasin-djuricic:feat/add-newrelic-integration

Conversation

@vukasin-djuricic
Copy link

Implements #2552

Summary

This PR adds a New Relic integration to SuperPlane, enabling users to react to alerts and query telemetry data from New Relic.

Authorization uses two keys:

  • User API Key — for NerdGraph/NRQL queries (no additional permissions needed)
  • License Key — for metric ingestion via the Metric API

Supports both US and EU data center regions.

Components

Name Type Description
Run NRQL Query Action Executes a NRQL query via the NerdGraph API. Supports configurable timeout (up to 120s).
Report Metric Action Sends custom metrics (gauge, count, summary) to the New Relic Metric API.
On Issue Trigger Receives New Relic alert issue webhooks. Validates payloads and optional Bearer token auth.

Implementation Details

  • Backend: pkg/integrations/newrelic/ — client, integration, two components, one trigger, webhook handler, example data
  • Frontend: web_src/src/pages/workflowv2/mappers/newrelic/ — mappers for all three components with execution details, metadata, and event sections
  • Docs: Auto-generated via make gen.components.docsdocs/components/NewRelic.mdx
  • Tests: Unit tests for Sync, Execute, Setup, webhook parsing, and edge cases across all components
  • HTTP safety: All HTTP requests use http.NewRequestWithContext to avoid context leaks
  • GraphQL validation: ValidateCredentials checks the response body for GraphQL errors (NerdGraph returns HTTP 200 even for auth failures)

Video Demo

https://youtu.be/_6P0luCxrpA

Checklist

  • Backend implementation in pkg/integrations/newrelic/
  • Frontend mappers in web_src/src/pages/workflowv2/mappers/newrelic/
  • Unit tests for all components and triggers
  • Example output data for components
  • Component documentation generated with make gen.components.docs
  • Integration icon (SVG) added and registered
  • Lint passes (revive)
  • All commits signed off (DCO)

Add a New Relic integration that enables users to react to alerts and
query telemetry data from New Relic.

Components:
- Run NRQL Query: execute NRQL queries via the NerdGraph API
- Report Metric: send custom metrics (gauge/count/summary) to the Metric API
- On Issue: webhook trigger for receiving New Relic alert issues

Implements superplanehq#2552

Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
@AleksandarCole AleksandarCole added the pr:stage-1/3 Needs to pass basic review. label Mar 2, 2026
Add missing mapstructure tags to Configuration struct for consistent
decoding, and quote AccountID in GraphQL query to avoid fragile coupling
with regex validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
vukasin-djuricic and others added 2 commits March 2, 2026 19:39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Revert quoteGraphQL on AccountID - NerdGraph account(id: Int!) expects
an unquoted integer, not a string. AccountID is already validated by
regex. Handle null config values in optionalIntegrationConfig to prevent
500 errors when webhookSecret is stored as null, matching the pattern
used by the Harness integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
vukasin-djuricic and others added 2 commits March 2, 2026 20:00
WebhookContext does not implement the GetBaseURL method added to the
NodeWebhookContext interface upstream. Use NodeWebhookContext to match
the pattern used by all other integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Apply filterEmptyStrings to config.Statuses consistently with how
config.Priorities is handled, preventing empty strings from matching
an empty payload.State.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
}

return string(value), nil
}
Copy link

Choose a reason for hiding this comment

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

Duplicated optionalIntegrationConfig across integration packages

Low Severity

optionalIntegrationConfig is duplicated between pkg/integrations/newrelic/on_issue.go and pkg/integrations/prometheus/on_alert.go, with subtly different error-handling logic. The newrelic version additionally suppresses "not a string" errors (for null config values), while the prometheus version only suppresses errors containing the config name and "not found". Having two divergent implementations of the same concept risks inconsistent behavior across integrations and makes future maintenance harder.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Author

Choose a reason for hiding this comment

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

Follows the pattern from the Harness integration, which also handles null config values. Happy to refactor into a shared utility if preferred.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Add required interval.ms field for count and summary metric types in
ReportMetric, as the New Relic Metric API rejects these without it.
Run prettier on on_issue.tsx and run_nrql_query.ts to fix formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
@AleksandarCole
Copy link
Collaborator

Hey @vukasin-djuricic, thanks for trying this one!

I tried testing this, and there are quite a few issues. It could be my lack of domain knowledge with New Relic, I will list them bellow, but I strongly encourage you joining our Discord as comms will be faster.

Action components do not work

Tried chaining together reportMetric and running NRQL Query to get said metric back, it returned null. Not sure which is failing - pushing data or fetching it
image

Trigger component doesn't work

Even though configured properly - the trigger component did not receive and show an incident that was triggered on New Relic

Trigger component webhook setup

It is possible to manage webhooks via UserAPI, there is no reason for us to push user to set up these things manually. Check dash0 pattern.

Those are only the critical ones, I will not list the UX improvements yet until the core functionality is achieved.

vukasin-djuricic and others added 2 commits March 4, 2026 17:06
…REATED status

- Auto-provision webhook destination + channel on NR via NerdGraph when
  OnIssue trigger is added (no more manual setup required)
- Cleanup destination + channel when trigger/integration is removed
- Handle 401/404 gracefully during cleanup to prevent infinite retry loops
- Use unique names (SuperPlane-<timestamp>) for destinations/channels
- Fix ReportMetric: coerce string values to numeric, add interval.ms for
  count/summary metrics, validate summary value shape in Setup()
- Fix RunNRQLQuery: remove debug logs, results no longer return null
- Add CREATED status to OnIssue valid statuses (NR sends CREATED on new issues)
- Simplify OnIssue Setup() to just validate config + RequestWebhook()
- Fix IntegrationDetails select field showing wrong default (EU→US race condition)
- Add zero-value ingestion delay hint in NRQL query results UI
- Update installation instructions to reflect auto-provisioning
- Delete mutations now follow NR docs (ids + error { details })

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
@vukasin-djuricic vukasin-djuricic force-pushed the feat/add-newrelic-integration branch from 35ac0a0 to e58ec1d Compare March 4, 2026 16:18
- Add CREATED to frontend stateLabels map (was missing despite backend support)
- Improve zero-value tip: only show when ALL result values are nullish, not any single one
- Add test for null webhookSecret config handling (prevents 500 on optional null fields)
- Remove hardcoded "SuperPlane" name from docs (channels now use unique timestamps)
- Regenerate component docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

vukasin-djuricic and others added 2 commits March 4, 2026 18:02
- Change NewRelicIssuePayload.AccountID from int to any, since NR
  sends accumulations.tag.account as a string in webhook payloads
- Update frontend type to accept number | string for accountId
- Fix zero-value tip: only treat null/undefined as empty, not 0
  (a legitimate count of 0 should not trigger the ingestion delay hint)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
Add defensive numeric validation in NewClient() so account ID is
verified before being interpolated into GraphQL queries, independent
of the Sync() validation path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: vumdjur <vdjuricic5322rn@raf.rs>
@vukasin-djuricic
Copy link
Author

GitHub PR komentar:

Update

All BugBot review comments addressed across 3 rounds:

Round 1: mapstructure tags ✅, null webhookSecret handling ✅ (+test), empty-string filtering ✅, interval.ms ✅, accountId quoting (false positive — used directly, not with quoteGraphQL) ✅,
duplicated optionalIntegrationConfig (acknowledged, out of scope)

Round 2: accountId changed from int→any (NR sends as string) ✅, zero-value tip fixed (only shows when ALL values null, not 0) ✅

Round 3: accountId validation added in NewClient() for defensive GraphQL injection prevention ✅

Video demo

https://www.youtube.com/watch?v=vfT2Lybz_cI

Pre-existing issue found during testing

CanvasCleanupWorker.processCanvas() (line 118) hard-deletes CanvasNode records directly instead of calling models.DeleteCanvasNode(). This means webhooks are not soft-deleted when a canvas is
deleted, so WebhookCleanupWorker never cleans up auto-provisioned resources (NR destinations/channels, LD webhooks, etc.) in external services. Affects all integrations with webhook handlers. Happy to
open a separate issue if helpful.

@AleksandarCole
Copy link
Collaborator

newrelic.onIssue provisioning is mostly working (destination/channel are created), but the default webhook payload template is brittle and can generate payloads that SuperPlane rejects (HTTP 400), especially in New Relic test/notification paths. As a result, real New Relic issues may fire while no trigger event appears in Canvas. The reportMetric and runNRQLQuery components are fine now; the gap is specifically in the onIssue webhook payload mapping/compatibility.

onIssue improvements

To properly fix the newrelic.onIssue trigger, we should harden both outbound payload generation and inbound parsing.

  • Harden the default New Relic channel payload template

    • Avoid brittle nested fields that may be missing.
    • Ensure generated JSON is always valid.
    • Provide safe defaults for optional fields.
  • Make HandleWebhook tolerant to payload variants

    • Normalize different shapes (string vs array, optional/missing fields).
    • Parse defensively before applying status/priority filters.
  • Validate provisioning during setup

    • After channel creation/update, run a test notification.
    • Fail setup with a clear actionable error if payload is invalid.
  • Migrate existing channels

    • Existing channels do not auto-update when template code changes.
    • Add a reprovision/update path so old channels receive the corrected template.
  • Improve observability

    • Log explicit webhook parse/validation failure reasons.
    • Make 400-class failures diagnosable from logs/UI.

Expected outcome

  • New Relic issues consistently reach SuperPlane.
  • Canvas trigger events are reliably created.
  • Fewer silent failures caused by template/payload mismatch.

@forestileao I need sanity check on this one ☝️
Also, I think we can start with code review.


@vukasin-djuricic some additional UX changes:

Integration setup

Since we are creating webhooks automatically - there's no need to have optional secret. Let's have a secret that is automatically set in the background and remove this input from UI.

onIssue

The details tab that I see and the one that is in your video look very different. Let's have details look like in your video with limited data, also let's just have single timestamp at the top instead of two of them.
image

Report metric and Run Query

For the Value configuration field we are using "expression" input - the same one that is used in filter and if components, this doesn't make sense since these inputs are for writing conditions, instead, use same input like you use for Metric Name.

Same for NRQL Query field

@AleksandarCole AleksandarCole added pr:stage-3/3 Ready for full, in-depth, review and removed pr:stage-1/3 Needs to pass basic review. labels Mar 4, 2026
Comment on lines +109 to +116
{
Name: "webhookSecret",
Label: "Webhook Secret",
Type: configuration.FieldTypeString,
Required: false,
Sensitive: true,
Description: "Optional secret for validating incoming webhook requests. If set, New Relic must send an Authorization: Bearer <secret> header.",
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if we always set an webhook secret. But not a webhook secret that the user provided? On webhook context from webhook handler it is possible to Get and Set secret using GetSecret and SetSecret. Then it would enforce to always use secure authenticated webhooks in Superplane.

We have some examples in the codebase (try searching by .SetSecret)

Comment on lines +81 to +88
{
Name: "timeout",
Label: "Timeout (seconds)",
Type: configuration.FieldTypeNumber,
Required: false,
Default: 30,
Description: "Query timeout in seconds",
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here it is possible to add
TypeOptions: &configuration.TypeOptions{ Number: &configuration.NumberTypeOptions{ Min: func() *int { min := 0; return &min }(), Max: func() *int { max := maxNRQLTimeout; return &max }(), }, },

So the UI or Agent will also know that 120 is the max timeout

Comment on lines +142 to +151
responseBody, err := client.NerdGraphQuery(context.Background(), graphQLQuery)
if err != nil {
return fmt.Errorf("failed to execute NRQL query: %v", err)
}

var response NerdGraphNRQLResponse
err = json.Unmarshal(responseBody, &response)
if err != nil {
return fmt.Errorf("error parsing response: %v", err)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice if every client response is already in the retuning struct. then you wouldn't need to unmarshal the json outside the client.

Comment on lines +196 to +214
type NerdGraphNRQLResponse struct {
Data struct {
Actor struct {
Account struct {
NRQL struct {
Results []any `json:"results"`
} `json:"nrql"`
} `json:"account"`
} `json:"actor"`
} `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}

func quoteGraphQL(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

And we could move this to the client.go as well

@forestileao
Copy link
Collaborator

@vukasin-djuricic The code looks good at all, just added minor comments

@vukasin-djuricic
Copy link
Author

Thanks for the thorough review! Working on the following based on your feedback:

Security / Webhook:

  • Switching to auto-generated secrets using GetSecret/SetSecret — removing the user-facing webhookSecret field entirely
  • Hardening the payload template with Handlebars conditionals for optional fields (policyName, conditionName)

Client refactor (forestileao):

  • Moving NRQL query execution logic to client.go
  • Returning typed structs from client methods instead of raw bytes
  • Adding NumberTypeOptions (min/max) for the timeout field

UX (AleksandarCole):

  • Changing value and query fields from expression to string/text input type
  • Cleaning up the details tab — single timestamp, less noise

Will push an update shortly.

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

Labels

edu pr:stage-3/3 Ready for full, in-depth, review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants