Skip to content

omnisignal Package

The omnisignal package provides an OmniSignal provider that normalizes Aha Ideas into enhancement_request signals for the ProductContext signal pipeline.

import (
    "github.com/plexusone/omnisignal"
    _ "github.com/grokify/aha-studio/omnisignal" // Register the "aha" provider
)

Overview

OmniSignal is a unified interface for ingesting operational signals (tickets, alerts, incidents, enhancement requests) from external systems into a canonical format for correlation and root-cause mapping. This package implements that interface for Aha!, so customer feedback captured as Aha Ideas can flow into the same pipeline as PagerDuty alerts, Jira tickets, and other signal sources.

The provider registers itself as "aha" via init() — importing the package for its side effect is enough to make it available through omnisignal.New/omnisignal.MustNew.

Quick Start

package main

import (
    "context"
    "log"
    "os"
    "time"

    "github.com/plexusone/omnisignal"
    _ "github.com/grokify/aha-studio/omnisignal" // Register the "aha" provider
)

func main() {
    provider, err := omnisignal.New("aha", omnisignal.Config{
        APIKey: os.Getenv("AHA_API_KEY"),
        Options: map[string]any{
            "subdomain": "mycompany",
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer provider.Close()

    signals, err := provider.Fetch(context.Background(), omnisignal.FetchOptions{
        Since: time.Now().AddDate(0, 0, -30),
        Limit: 100,
    })
    if err != nil {
        log.Fatal(err)
    }

    for _, sig := range signals {
        log.Printf("%s: %s (%d votes)", sig.ID, sig.Summary, sig.Metadata["votes"])
    }
}

Configuration

omnisignal.Config fields the Aha provider reads:

Field Required Description
APIKey Yes Aha API key
BaseURL No Full API base URL override
Options["subdomain"] No Aha subdomain, used to build the API URL when BaseURL isn't set
Options["capability_mappings"] No map[string]string mapping Aha category names to capability refs (e.g. "Integrations": "capability:integrations"), attached to signals as metadata["capability_ref"]
Options["customer_mappings"] No Accepted for cross-provider consistency but not yet applied by this provider

Either BaseURL or Options["subdomain"] must resolve to a valid Aha account; aha.NewClient also falls back to the AHA_SUBDOMAIN environment variable if neither is set.

Fetching Signals

Fetch lists Aha Ideas and converts each to a signal.Signal, paginating internally until FetchOptions.Limit is reached or all pages are consumed:

FetchOptions field Maps to
Since aha.WithIdeaCreatedSince
Until aha.WithIdeaCreatedBefore
Limit Stops fetching once this many signals are collected
Filters["workflow_status"] aha.WithIdeaStatus
Filters["query"] aha.WithIdeaQuery

Subscribe is not supported (Aha has no real-time push API for ideas) and returns omnisignal.ErrNotSupported.

Signal Mapping

Each Aha Idea becomes one signal.Signal of type signal.TypeEnhancementRequest:

Signal field Source
ID "aha-" + idea.ReferenceNum
Status Derived from idea.WorkflowStatus.Name — see status mapping below
Source {Type: "product_management", Name: "aha", ExternalID: idea.ReferenceNum}
Domain {Name: "product", Subdomain: <normalized first category>}
Severity Derived from idea.Votes — see severity mapping below
Summary / Description idea.Name / idea.Description
Entities One common.Entity{Type: "category", ...} per Aha category, with Ref set when a capability_mappings entry matches
ObservedAt idea.CreatedAt
Metadata votes, aha_reference_num, aha_score, curated: true, capability_ref (if mapped), and aha_feature_id/aha_feature_ref/aha_feature_name when the idea is linked to a feature
Fingerprint Computed via signal.ComputeFingerprint; silently left empty if computation fails

Workflow status → signal status

Substring match (case-insensitive) against the Aha workflow status name:

Contains Signal status
shipped, complete, done signal.StatusArchived
progress, building, planned signal.StatusProcessing
won't, archived, declined signal.StatusIgnored
(anything else, or no status) signal.StatusNew

Votes → severity

Votes Severity
≥ 50 common.SeverityCritical
≥ 20 common.SeverityHigh
≥ 5 common.SeverityMedium
≥ 1 common.SeverityLow
0 common.SeverityInfo

This is a rough, tunable heuristic — it isn't Aha's own prioritization data, just a way to give freshly-ingested signals a reasonable starting severity.

Capabilities

Capabilities{
    SupportsStreaming:   false,
    SupportsBatchFetch:  true,
    SupportsFiltering:   true,
    SupportsAcknowledge: false,
    MaxBatchSize:        200,
    RateLimitPerMinute:  300,
    SignalTypes:         []signal.Type{signal.TypeEnhancementRequest},
}

API Reference

See pkg.go.dev/github.com/grokify/aha-studio/omnisignal and the omnisignal / signal-spec packages it implements.