Repository: PagerDuty/go-pagerduty
Pull request #481 (2023-05-04 07:55:32)

Add raw incidents metrics
Implement endpoints for single and multiple raw incident metrics

https://developer.pagerduty.com/api-reference/b3A6Mjc0ODExMw-get-raw-data-single-incident

https://developer.pagerduty.com/api-reference/c2d493e995071-get-raw-data-multiple-incidents

This was previously done in PRs #347 and #422 which have gone stale.

krav: @ChuckCrawford @theckman Hi! Can we get this reviewed? Been looking to use this endpoint. Thanks!
MaggotMouth: Thanks for the review @ChuckCrawford - I've made the changes as requested and updated the associated tests.

Patch:
diff --git a/analytics.go b/analytics.go
index ea6ce15..13cc72e 100644
--- a/analytics.go
+++ b/analytics.go
@@ -6,6 +6,7 @@ import (
 )
 
 const analyticsBaseURL = "/analytics/metrics/incidents"
+const rawDataBaseURL = "/analytics/raw"
 
 // AnalyticsRequest represents the request to be sent to PagerDuty when you want
 // aggregated analytics.
@@ -15,6 +16,18 @@ type AnalyticsRequest struct {
 	TimeZone      string           `json:"time_zone,omitempty"`
 }
 
+// AnalyticsRawIncidentsRequest represents the request to be sent to PagerDuty when you want
+// raw analytics.
+type AnalyticsRawIncidentsRequest struct {
+	Filters       *AnalyticsFilter `json:"filters,omitempty"`
+	StartingAfter string           `json:"starting_after,omitempty"`
+	EndingBefore  string           `json:"ending_before,omitempty"`
+	Limit         int              `json:"limit,omitempty"`
+	Order         string           `json:"order,omitempty"`
+	OrderBy       string           `json:"order_by,omitempty"`
+	TimeZone      string           `json:"time_zone,omitempty"`
+}
+
 // AnalyticsResponse represents the response from the PagerDuty API.
 type AnalyticsResponse struct {
 	Data          []AnalyticsData  `json:"data,omitempty"`
@@ -23,6 +36,19 @@ type AnalyticsResponse struct {
 	TimeZone      string           `json:"time_zone,omitempty"`
 }
 
+// AnalyticsRawIncidentsResponse represents the response from the PagerDuty API.
+type AnalyticsRawIncidentsResponse struct {
+	First    string                 `json:"first,omitempty"`
+	Last     string                 `json:"last,omitempty"`
+	Limit    int                    `json:"limit,omitempty"`
+	More     bool                   `json:"more,omitempty"`
+	Order    string                 `json:"order,omitempty"`
+	OrderBy  string                 `json:"order_by,omitempty"`
+	Filters  *AnalyticsFilter       `json:"filters,omitempty"`
+	TimeZone string                 `json:"time_zone,omitempty"`
+	Data     []AnalyticsRawIncident `json:"data,omitempty"`
+}
+
 // AnalyticsFilter represents the set of filters as part of the request to PagerDuty when
 // requesting analytics.
 type AnalyticsFilter struct {
@@ -36,7 +62,7 @@ type AnalyticsFilter struct {
 	PriorityNames  []string `json:"priority_names,omitempty"`
 }
 
-// AnalyticsData represents the structure of the analytics we have available.
+// AnalyticsData represents the structure of the aggregated analytics we have available.
 type AnalyticsData struct {
 	ServiceID                      string  `json:"service_id,omitempty"`
 	ServiceName                    string  `json:"service_name,omitempty"`
@@ -61,6 +87,36 @@ type AnalyticsData struct {
 	RangeStart                     string  `json:"range_start,omitempty"`
 }
 
+// AnalyticsRawIncident represents the structure of the raw incident analytics we have available.
+type AnalyticsRawIncident struct {
+	AssignmentCount           int    `json:"assignment_count,omitempty"`
+	BusinessHourInterruptions int    `json:"business_hour_interruptions,omitempty"`
+	CreatedAt                 string `json:"created_at,omitempty"`
+	Description               string `json:"description,omitempty"`
+	EngagedSeconds            int    `json:"engaged_seconds,omitempty"`
+	EngagedUserCount          int    `json:"engaged_user_count,omitempty"`
+	EscalationCount           int    `json:"escalation_count,omitempty"`
+	ID                        string `json:"id,omitempty"`
+	IncidentNumber            int    `json:"incident_number,omitempty"`
+	IsMajor                   bool   `json:"major,omitempty"`
+	OffHourInterruptions      int    `json:"off_hour_interruptions,omitempty"`
+	PriorityID                string `json:"priority_id,omitempty"`
+	PriorityName              string `json:"priority_name,omitempty"`
+	ResolvedAt                string `json:"resolved_at,omitempty"`
+	SecondsToEngage           int    `json:"seconds_to_engage,omitempty"`
+	SecondsToFirstAck         int    `json:"seconds_to_first_ack,omitempty"`
+	SecondsToMobilize         int    `json:"seconds_to_mobilize,omitempty"`
+	SecondsToResolve          int    `json:"seconds_to_resolve,omitempty"`
+	ServiceID                 string `json:"service_id,omitempty"`
+	ServiceName               string `json:"service_name,omitempty"`
+	SleepHourInterruptions    int    `json:"sleep_hour_interruptions,omitempty"`
+	SnoozedSeconds            int    `json:"snoozed_seconds,omitempty"`
+	TeamID                    string `json:"team_id,omitempty"`
+	TeamName                  string `json:"team_name,omitempty"`
+	Urgency                   string `json:"urgency,omitempty"`
+	UserDefinedEffortSeconds  int    `json:"user_defined_effort_seconds,omitempty"`
+}
+
 // GetAggregatedIncidentData gets the aggregated incident analytics for the requested data.
 func (c *Client) GetAggregatedIncidentData(ctx context.Context, analytics AnalyticsRequest) (AnalyticsResponse, error) {
 	return c.getAggregatedData(ctx, analytics, "all")
@@ -94,3 +150,39 @@ func (c *Client) getAggregatedData(ctx context.Context, analytics AnalyticsReque
 
 	return analyticsResponse, nil
 }
+
+// GetAnalyticsIncidentsById gets the raw analytics for the requested incident.
+func (c *Client) GetAnalyticsIncidentsById(ctx context.Context, id string) (*AnalyticsRawIncident, error) {
+	path := fmt.Sprintf("%s/%s/%s", rawDataBaseURL, "incidents", id)
+	resp, err := c.get(ctx, path, nil)
+
+	if err != nil {
+		return &AnalyticsRawIncident{}, err
+	}
+
+	var rawData AnalyticsRawIncident
+	if err = c.decodeJSON(resp, &rawData); err != nil {
+		return &AnalyticsRawIncident{}, err
+	}
+
+	return &rawData, nil
+}
+
+// GetAnalyticsIncidents gets the raw analytics for the requested data.
+func (c *Client) GetAnalyticsIncidents(ctx context.Context, rawDataReq AnalyticsRawIncidentsRequest) (*AnalyticsRawIncidentsResponse, error) {
+	h := map[string]string{}
+
+	path := fmt.Sprintf("%s/%s", rawDataBaseURL, "incidents")
+	resp, err := c.post(ctx, path, rawDataReq, h)
+
+	if err != nil {
+		return &AnalyticsRawIncidentsResponse{}, err
+	}
+
+	var rawDataResponse AnalyticsRawIncidentsResponse
+	if err = c.decodeJSON(resp, &rawDataResponse); err != nil {
+		return &AnalyticsRawIncidentsResponse{}, err
+	}
+
+	return &rawDataResponse, nil
+}


Test patch:
diff --git a/analytics_test.go b/analytics_test.go
index 07c3fb5..78fe36a 100644
--- a/analytics_test.go
+++ b/analytics_test.go
@@ -3,6 +3,7 @@ package pagerduty
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"testing"
 )
@@ -139,3 +140,143 @@ func TestAnalytics_GetAggregatedTeamData(t *testing.T) {
 	}
 	testEqual(t, want, res)
 }
+
+func TestAnalytics_GetAnalyticsIncidentsById(t *testing.T) {
+	setup()
+	defer teardown()
+
+	id := "PFGEDX0"
+	rawDataWanted := AnalyticsRawIncident{
+		AssignmentCount:           10,
+		BusinessHourInterruptions: 1,
+		CreatedAt:                 "2021-01-06T00:00:00.000000",
+		Description:               "Test Alert Description",
+		EngagedSeconds:            404,
+		EngagedUserCount:          2,
+		EscalationCount:           1,
+		ID:                        id,
+		IncidentNumber:            324,
+		IsMajor:                   true,
+		OffHourInterruptions:      1,
+		PriorityID:                "PSVSDF3",
+		PriorityName:              "Urgent",
+		ResolvedAt:                "2021-01-06T08:23:21.000000",
+		SecondsToEngage:           3453,
+		SecondsToFirstAck:         123,
+		SecondsToMobilize:         435,
+		SecondsToResolve:          324524,
+		ServiceID:                 "PA4DFYS",
+		ServiceName:               "My Test Service",
+		SleepHourInterruptions:    0,
+		SnoozedSeconds:            0,
+		TeamID:                    "PCDYDX0",
+		TeamName:                  "My Team",
+		Urgency:                   "Urgent",
+		UserDefinedEffortSeconds:  0,
+	}
+
+	bytesRawDataWanted, err := json.Marshal(rawDataWanted)
+	testErrCheck(t, "json.Marshal()", "", err)
+
+	url := fmt.Sprintf("%s/%s", "/analytics/raw/incidents", id)
+	mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write(bytesRawDataWanted)
+	})
+
+	client := defaultTestClient(server.URL, "foo")
+
+	res, err := client.GetAnalyticsIncidentsById(context.Background(), id)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, &rawDataWanted, res)
+}
+
+func TestAnalytics_GetAnalyticsIncidents(t *testing.T) {
+	setup()
+	defer teardown()
+
+	id := "PFGEDX0"
+	rawDataWanted := AnalyticsRawIncident{
+		AssignmentCount:           10,
+		BusinessHourInterruptions: 1,
+		CreatedAt:                 "2021-01-06T00:00:00.000000",
+		Description:               "Test Alert Description",
+		EngagedSeconds:            404,
+		EngagedUserCount:          2,
+		EscalationCount:           1,
+		ID:                        id,
+		IncidentNumber:            324,
+		IsMajor:                   true,
+		OffHourInterruptions:      1,
+		PriorityID:                "PSVSDF3",
+		PriorityName:              "Urgent",
+		ResolvedAt:                "2021-01-06T08:23:21.000000",
+		SecondsToEngage:           3453,
+		SecondsToFirstAck:         123,
+		SecondsToMobilize:         435,
+		SecondsToResolve:          324524,
+		ServiceID:                 "PA4DFYS",
+		ServiceName:               "My Test Service",
+		SleepHourInterruptions:    0,
+		SnoozedSeconds:            0,
+		TeamID:                    "PCDYDX0",
+		TeamName:                  "My Team",
+		Urgency:                   "Urgent",
+		UserDefinedEffortSeconds:  0,
+	}
+
+	rawDataRequest := AnalyticsRawIncidentsRequest{
+		Filters: &AnalyticsFilter{
+			CreatedAtStart: "2021-01-01T15:00:32Z",
+			CreatedAtEnd:   "2021-01-08T15:00:32Z",
+			TeamIDs:        []string{"PCDYDX0"},
+		},
+		StartingAfter: "eyJpZCI6IlEwTUlFTUtXTVNYOFZFIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTMwVDA4OjU0OjAzIn0=",
+		EndingBefore:  "eyJpZCI6IlEzQU1DSlhWU05HTjZEIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTI2VDE4OjA3OjIyIn0=",
+		Limit:         100,
+		Order:         "asc",
+		OrderBy:       "created_at",
+		TimeZone:      "Etc/UTC",
+	}
+	rawFilterWanted := AnalyticsFilter{CreatedAtStart: "2021-01-06T09:21:41Z", CreatedAtEnd: "2021-01-13T09:21:41Z", TeamIDs: []string{"PCDYDX0"}}
+	rawDataResponse := AnalyticsRawIncidentsResponse{
+		Data:     []AnalyticsRawIncident{rawDataWanted},
+		Filters:  &rawFilterWanted,
+		First:    "eyJpZCI6IlEwTUlFTUtXTVNYOFZFIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTMwVDA4OjU0OjAzIn0=",
+		Last:     "eyJpZCI6IlEzQU1DSlhWU05HTjZEIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTI2VDE4OjA3OjIyIn0=",
+		Limit:    100,
+		More:     true,
+		Order:    "asc",
+		OrderBy:  "created_at",
+		TimeZone: "Etc/UTC",
+	}
+
+	bytesRawDataWanted, err := json.Marshal(rawDataResponse)
+	testErrCheck(t, "json.Marshal()", "", err)
+
+	mux.HandleFunc("/analytics/raw/incidents", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		_, _ = w.Write(bytesRawDataWanted)
+	})
+
+	client := defaultTestClient(server.URL, "foo")
+
+	res, err := client.GetAnalyticsIncidents(context.Background(), rawDataRequest)
+	want := &AnalyticsRawIncidentsResponse{
+		Data:     []AnalyticsRawIncident{rawDataWanted},
+		Filters:  &rawFilterWanted,
+		First:    "eyJpZCI6IlEwTUlFTUtXTVNYOFZFIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTMwVDA4OjU0OjAzIn0=",
+		Last:     "eyJpZCI6IlEzQU1DSlhWU05HTjZEIiwib3JkZXJfYnkiOiJjcmVhdGVkX2F0IiwidmFsdWUiOiIyMDIzLTA0LTI2VDE4OjA3OjIyIn0=",
+		Limit:    100,
+		More:     true,
+		Order:    "asc",
+		OrderBy:  "created_at",
+		TimeZone: "Etc/UTC",
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}