Repository: newrelic/newrelic-client-go (go)

Issue:
Migrate Components, Component Metrics, and Component Metrics Data
### Objective
Migrate [components.go](https://github.com/paultyng/go-newrelic/blob/master/api/components.go), [component_metrics.go](https://github.com/paultyng/go-newrelic/blob/master/api/component_metrics.go), and [component_metric_data.go](https://github.com/paultyng/go-newrelic/blob/master/api/component_metric_data.go) from `go-newrelic` to our project.

### Acceptance Criteria
- Migrate the code and make any reasonable updates to the implementation pattern
    - Note: The design pattern should be agreed upon by the dev team and should be consistent with all other features/resources
- Include tests

Pull request:
feat: add components
Resolves #17 .

This PR introduces support for Plugins components and all associated endpoints.

Patch:
diff --git a/build/lint.mk b/build/lint.mk
index 7571f1e..f2242b7 100644
--- a/build/lint.mk
+++ b/build/lint.mk
@@ -6,6 +6,7 @@ GO           ?= go
 MISSPELL     ?= misspell
 GOFMT        ?= gofmt
 
+EXCLUDEDIR   ?= .git
 SRCDIR       ?= .
 GO_PKGS      ?= $(shell ${GO} list ./... | grep -v -e "/vendor/" -e "/example")
 FILES        ?= $(shell find ${SRCDIR} -type f | grep -v -e '.git/' -e '/vendor/')
@@ -33,11 +34,11 @@ spell-check-fix: deps
 
 gofmt: deps
 	@echo "=== $(PROJECT_NAME) === [ gofmt            ]: Checking file format with $(GOFMT)..."
-	@$(GOFMT) -e -l -s -d ${SRCDIR}
+	@find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -d ${SRCDIR}
 
 gofmt-fix: deps
 	@echo "=== $(PROJECT_NAME) === [ gofmt-fix        ]: Fixing file format with $(GOFMT)..."
-	@$(GOFMT) -e -l -s -w ${SRCDIR}
+	@find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 $(GOFMT) -e -l -s -w ${SRCDIR}
 
 govet: deps
 	@echo "=== $(PROJECT_NAME) === [ govet            ]: Checking file format with $(GO) vet..."
@@ -53,7 +54,7 @@ ineffassign: deps
 
 gocyclo: deps
 	@echo "=== $(PROJECT_NAME) === [ gocyclo          ]: Calculating cyclomatic complexities of functions (gocyclo)..."
-	@gocyclo -over 20 $(SRCDIR)
+	@find . -path "$(EXCLUDEDIR)" -prune -print0 | xargs -0 gocyclo -over 20 $(SRCDIR)
 
 bodyclose: deps
 	@echo "=== $(PROJECT_NAME) === [ bodyclose        ]: Checking that http response bodies are closed (bodyclose)..."
diff --git a/go.mod b/go.mod
index 87e4253..446b552 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
 	github.com/golangci/golangci-lint v1.21.0
 	github.com/google/go-cmp v0.2.0
+	github.com/google/go-querystring v1.0.0
 	github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8
 	github.com/hashicorp/go-retryablehttp v0.6.4
 	github.com/remyoudompheng/go-misc v0.0.0-20190427085024-2d6ac652a50e
diff --git a/go.sum b/go.sum
index 9bb9620..e2b2337 100644
--- a/go.sum
+++ b/go.sum
@@ -123,6 +123,8 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 h1:ehVe1P3MbhHjeN/Rn66N2fGLrP85XXO1uxpLhv0jtX8=
 github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
diff --git a/internal/http/client.go b/internal/http/client.go
index a21f685..709cbd7 100644
--- a/internal/http/client.go
+++ b/internal/http/client.go
@@ -9,7 +9,9 @@ import (
 	neturl "net/url"
 	"time"
 
+	"github.com/google/go-querystring/query"
 	retryablehttp "github.com/hashicorp/go-retryablehttp"
+
 	"github.com/newrelic/newrelic-client-go/internal/version"
 	"github.com/newrelic/newrelic-client-go/pkg/config"
 )
@@ -83,7 +85,7 @@ func (c *NewRelicClient) SetErrorValue(v ErrorResponse) *NewRelicClient {
 // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
 func (c *NewRelicClient) Get(
 	url string,
-	queryParams *map[string]string,
+	queryParams interface{},
 	respBody interface{},
 ) (*http.Response, error) {
 	return c.do(http.MethodGet, url, queryParams, nil, respBody)
@@ -96,11 +98,11 @@ func (c *NewRelicClient) Get(
 // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
 func (c *NewRelicClient) Post(
 	url string,
-	params *map[string]string,
+	queryParams interface{},
 	reqBody interface{},
 	respBody interface{},
 ) (*http.Response, error) {
-	return c.do(http.MethodPost, url, params, reqBody, respBody)
+	return c.do(http.MethodPost, url, queryParams, reqBody, respBody)
 }
 
 // Put represents an HTTP PUT request to a New Relic API.
@@ -110,7 +112,7 @@ func (c *NewRelicClient) Post(
 // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
 func (c *NewRelicClient) Put(
 	url string,
-	queryParams *map[string]string,
+	queryParams interface{},
 	reqBody interface{},
 	respBody interface{},
 ) (*http.Response, error) {
@@ -122,7 +124,7 @@ func (c *NewRelicClient) Put(
 // The respBody argument will be unmarshaled from JSON in the response body to the type provided.
 // If respBody is not nil and the response body cannot be unmarshaled to the type provided, an error will be returned.
 func (c *NewRelicClient) Delete(url string,
-	queryParams *map[string]string,
+	queryParams interface{},
 	respBody interface{},
 ) (*http.Response, error) {
 	return c.do(http.MethodDelete, url, queryParams, nil, respBody)
@@ -149,15 +151,20 @@ func (c *NewRelicClient) setHeaders(req *retryablehttp.Request) {
 	req.Header.Set("User-Agent", c.Config.UserAgent)
 }
 
-func setQueryParams(req *retryablehttp.Request, params *map[string]string) {
-	if params != nil {
-		q := req.URL.Query()
-		for k, v := range *params {
-			q.Add(k, v)
-		}
+func setQueryParams(req *retryablehttp.Request, params interface{}) error {
+	if params == nil || len(req.URL.Query()) > 0 {
+		return nil
+	}
 
-		req.URL.RawQuery = q.Encode()
+	q, err := query.Values(params)
+
+	if err != nil {
+		return err
 	}
+
+	req.URL.RawQuery = q.Encode()
+
+	return nil
 }
 
 func (c *NewRelicClient) makeURL(url string) (*neturl.URL, error) {
@@ -183,7 +190,7 @@ func (c *NewRelicClient) makeURL(url string) (*neturl.URL, error) {
 func (c *NewRelicClient) do(
 	method string,
 	url string,
-	params *map[string]string,
+	params interface{},
 	reqBody interface{},
 	value interface{},
 ) (*http.Response, error) {
@@ -206,7 +213,11 @@ func (c *NewRelicClient) do(
 	}
 
 	c.setHeaders(req)
-	setQueryParams(req, params)
+	err = setQueryParams(req, params)
+
+	if err != nil {
+		return nil, err
+	}
 
 	resp, retryErr := c.Client.Do(req)
 
diff --git a/pkg/alerts/alert_conditions.go b/pkg/alerts/alert_conditions.go
index edb15cb..1bae24c 100644
--- a/pkg/alerts/alert_conditions.go
+++ b/pkg/alerts/alert_conditions.go
@@ -2,15 +2,14 @@ package alerts
 
 import (
 	"fmt"
-	"strconv"
 )
 
 // ListAlertConditions returns alert conditions for a specified policy.
 func (alerts *Alerts) ListAlertConditions(policyID int) ([]*AlertCondition, error) {
 	response := alertConditionsResponse{}
 	alertConditions := []*AlertCondition{}
-	queryParams := map[string]string{
-		"policy_id": strconv.Itoa(policyID),
+	queryParams := listAlertConditionsParams{
+		PolicyID: policyID,
 	}
 
 	nextURL := "/alerts_conditions.json"
@@ -99,6 +98,10 @@ func (alerts *Alerts) DeleteAlertCondition(id int) (*AlertCondition, error) {
 	return &resp.Condition, nil
 }
 
+type listAlertConditionsParams struct {
+	PolicyID int `url:"policy_id,omitempty"`
+}
+
 type alertConditionsResponse struct {
 	Conditions []*AlertCondition `json:"conditions,omitempty"`
 }
diff --git a/pkg/alerts/alert_policies.go b/pkg/alerts/alert_policies.go
index 7a2fe4a..f9477a4 100644
--- a/pkg/alerts/alert_policies.go
+++ b/pkg/alerts/alert_policies.go
@@ -7,7 +7,7 @@ import (
 // ListAlertPoliciesParams represents a set of filters to be
 // used when querying New Relic alert policies.
 type ListAlertPoliciesParams struct {
-	Name string
+	Name string `url:"filter[name],omitempty"`
 }
 
 // ListAlertPolicies returns a list of Alert Policies for a given account.
@@ -15,10 +15,9 @@ func (alerts *Alerts) ListAlertPolicies(params *ListAlertPoliciesParams) ([]Aler
 	response := alertPoliciesResponse{}
 	alertPolicies := []AlertPolicy{}
 	nextURL := "/alerts_policies.json"
-	paramsMap := buildListAlertPoliciesParamsMap(params)
 
 	for nextURL != "" {
-		resp, err := alerts.client.Get(nextURL, &paramsMap, &response)
+		resp, err := alerts.client.Get(nextURL, &params, &response)
 
 		if err != nil {
 			return nil, err
@@ -98,20 +97,6 @@ func (alerts *Alerts) DeleteAlertPolicy(id int) (*AlertPolicy, error) {
 	return &resp.Policy, nil
 }
 
-func buildListAlertPoliciesParamsMap(params *ListAlertPoliciesParams) map[string]string {
-	paramsMap := map[string]string{}
-
-	if params == nil {
-		return paramsMap
-	}
-
-	if params.Name != "" {
-		paramsMap["filter[name]"] = params.Name
-	}
-
-	return paramsMap
-}
-
 type alertPoliciesResponse struct {
 	Policies []AlertPolicy `json:"policies,omitempty"`
 }
diff --git a/pkg/alerts/alert_policy_channels.go b/pkg/alerts/alert_policy_channels.go
index 11ba7d0..092fba9 100644
--- a/pkg/alerts/alert_policy_channels.go
+++ b/pkg/alerts/alert_policy_channels.go
@@ -2,7 +2,6 @@ package alerts
 
 import (
 	"strconv"
-	"strings"
 )
 
 // UpdatePolicyChannels updates a policy by adding the specified notification channels.
@@ -13,9 +12,9 @@ func (alerts *Alerts) UpdatePolicyChannels(policyID int, channelIDs []int) (*Pol
 		channelIDStrings[i] = strconv.Itoa(channelID)
 	}
 
-	queryParams := map[string]string{
-		"policy_id":   strconv.Itoa(policyID),
-		"channel_ids": strings.Join(channelIDStrings, ","),
+	queryParams := updatePolicyChannelsParams{
+		PolicyID:   policyID,
+		ChannelIDs: channelIDs,
 	}
 
 	resp := updatePolicyChannelsResponse{}
@@ -32,9 +31,9 @@ func (alerts *Alerts) UpdatePolicyChannels(policyID int, channelIDs []int) (*Pol
 // DeletePolicyChannel deletes a notification channel from an alert policy.
 // This method returns a response containing the AlertChannel that was deleted from the policy.
 func (alerts *Alerts) DeletePolicyChannel(policyID int, channelID int) (*AlertChannel, error) {
-	queryParams := map[string]string{
-		"policy_id":  strconv.Itoa(policyID),
-		"channel_id": strconv.Itoa(channelID),
+	queryParams := deletePolicyChannelsParams{
+		PolicyID:  policyID,
+		ChannelID: channelID,
 	}
 
 	resp := deletePolicyChannelResponse{}
@@ -48,6 +47,16 @@ func (alerts *Alerts) DeletePolicyChannel(policyID int, channelID int) (*AlertCh
 	return &resp.Channel, nil
 }
 
+type updatePolicyChannelsParams struct {
+	PolicyID   int   `url:"policy_id,omitempty"`
+	ChannelIDs []int `url:"channel_ids,omitempty"`
+}
+
+type deletePolicyChannelsParams struct {
+	PolicyID  int `url:"policy_id,omitempty"`
+	ChannelID int `url:"channel_id,omitempty"`
+}
+
 type updatePolicyChannelsResponse struct {
 	Policy PolicyChannels `json:"policy,omitempty"`
 }
diff --git a/pkg/alerts/infrastructure_conditions.go b/pkg/alerts/infrastructure_conditions.go
index 640219f..9e348d1 100644
--- a/pkg/alerts/infrastructure_conditions.go
+++ b/pkg/alerts/infrastructure_conditions.go
@@ -2,14 +2,15 @@ package alerts
 
 import (
 	"fmt"
-	"strconv"
 )
 
 // ListInfrastructureConditions is used to retrieve New Relic Infrastructure alert conditions.
 func (a *Alerts) ListInfrastructureConditions(policyID int) ([]InfrastructureCondition, error) {
 	resp := infrastructureConditionsResponse{}
-	paramsMap := map[string]string{"policy_id": strconv.Itoa(policyID)}
-	_, err := a.infraClient.Get("/alerts/conditions", &paramsMap, &resp)
+	queryParams := listInfrastructureConditionsParams{
+		PolicyID: policyID,
+	}
+	_, err := a.infraClient.Get("/alerts/conditions", &queryParams, &resp)
 
 	if err != nil {
 		return nil, err
@@ -72,6 +73,10 @@ func (a *Alerts) DeleteInfrastructureCondition(conditionID int) error {
 	return nil
 }
 
+type listInfrastructureConditionsParams struct {
+	PolicyID int `url:"policy_id,omitempty"`
+}
+
 type infrastructureConditionsResponse struct {
 	Conditions []InfrastructureCondition `json:"data,omitempty"`
 }
diff --git a/pkg/alerts/nrql_alert_conditions.go b/pkg/alerts/nrql_alert_conditions.go
index 8a2fd61..c58140d 100644
--- a/pkg/alerts/nrql_alert_conditions.go
+++ b/pkg/alerts/nrql_alert_conditions.go
@@ -2,15 +2,14 @@ package alerts
 
 import (
 	"fmt"
-	"strconv"
 )
 
 // ListNrqlAlertConditions returns NRQL alert conditions for a specified policy.
 func (alerts *Alerts) ListNrqlAlertConditions(policyID int) ([]*NrqlCondition, error) {
 	response := nrqlConditionsResponse{}
 	conditions := []*NrqlCondition{}
-	queryParams := map[string]string{
-		"policy_id": strconv.Itoa(policyID),
+	queryParams := listNrqlAlertConditionsParams{
+		PolicyID: policyID,
 	}
 
 	nextURL := "/alerts_nrql_conditions.json"
@@ -104,6 +103,10 @@ func (alerts *Alerts) DeleteNrqlAlertCondition(id int) (*NrqlCondition, error) {
 	return &resp.NrqlCondition, nil
 }
 
+type listNrqlAlertConditionsParams struct {
+	PolicyID int `url:"policy_id,omitempty"`
+}
+
 type nrqlConditionsResponse struct {
 	NrqlConditions []*NrqlCondition `json:"nrql_conditions,omitempty"`
 }
diff --git a/pkg/alerts/synthetics_conditions.go b/pkg/alerts/synthetics_conditions.go
index 8e8c801..1374b21 100644
--- a/pkg/alerts/synthetics_conditions.go
+++ b/pkg/alerts/synthetics_conditions.go
@@ -2,7 +2,6 @@ package alerts
 
 import (
 	"fmt"
-	"strconv"
 )
 
 // ListSyntheticsConditions returns a list of Synthetics alert conditions for a given policy.
@@ -10,12 +9,12 @@ func (alerts *Alerts) ListSyntheticsConditions(policyID int) ([]SyntheticsCondit
 	response := syntheticsConditionsResponse{}
 	conditions := []SyntheticsCondition{}
 	nextURL := fmt.Sprintf("/alerts_synthetics_conditions.json")
-	params := map[string]string{
-		"policy_id": strconv.Itoa(policyID),
+	queryParams := listSyntheticsConditionsParams{
+		PolicyID: policyID,
 	}
 
 	for nextURL != "" {
-		resp, err := alerts.client.Get(nextURL, &params, &response)
+		resp, err := alerts.client.Get(nextURL, &queryParams, &response)
 
 		if err != nil {
 			return nil, err
@@ -71,6 +70,10 @@ func (alerts *Alerts) DeleteSyntheticsCondition(conditionID int) (*SyntheticsCon
 	return &resp.Condition, nil
 }
 
+type listSyntheticsConditionsParams struct {
+	PolicyID int `url:"policy_id,omitempty"`
+}
+
 type syntheticsConditionsResponse struct {
 	Conditions []SyntheticsCondition `json:"synthetics_conditions,omitempty"`
 }
diff --git a/pkg/apm/applications.go b/pkg/apm/applications.go
index 5c5ded7..5407a04 100644
--- a/pkg/apm/applications.go
+++ b/pkg/apm/applications.go
@@ -2,17 +2,15 @@ package apm
 
 import (
 	"fmt"
-	"strconv"
-	"strings"
 )
 
 // ListApplicationsParams represents a set of filters to be
 // used when querying New Relic applications.
 type ListApplicationsParams struct {
-	Name     *string
-	Host     *string
-	IDs      []int
-	Language *string
+	Name     string `url:"filter[name],omitempty"`
+	Host     string `url:"filter[host],omitempty"`
+	IDs      []int  `url:"filter[ids],omitempty,comma"`
+	Language string `url:"filter[language],omitempty"`
 }
 
 // ListApplications is used to retrieve New Relic applications.
@@ -20,10 +18,9 @@ func (apm *APM) ListApplications(params *ListApplicationsParams) ([]Application,
 	response := applicationsResponse{}
 	apps := []Application{}
 	nextURL := "/applications.json"
-	paramsMap := buildListApplicationsParamsMap(params)
 
 	for nextURL != "" {
-		resp, err := apm.client.Get(nextURL, &paramsMap, &response)
+		resp, err := apm.client.Get(nextURL, &params, &response)
 
 		if err != nil {
 			return nil, err
@@ -91,34 +88,6 @@ func (apm *APM) DeleteApplication(applicationID int) (*Application, error) {
 	return &response.Application, nil
 }
 
-func buildListApplicationsParamsMap(params *ListApplicationsParams) map[string]string {
-	paramsMap := map[string]string{}
-
-	if params != nil {
-		if params.Name != nil {
-			paramsMap["filter[name]"] = *params.Name
-		}
-
-		if params.Host != nil {
-			paramsMap["filter[host]"] = *params.Host
-		}
-
-		if params.IDs != nil {
-			ids := []string{}
-			for _, id := range params.IDs {
-				ids = append(ids, strconv.Itoa(id))
-			}
-			paramsMap["filter[ids]"] = strings.Join(ids, ",")
-		}
-
-		if params.Language != nil {
-			paramsMap["filter[language]"] = *params.Language
-		}
-	}
-
-	return paramsMap
-}
-
 type applicationsResponse struct {
 	Applications []Application `json:"applications,omitempty"`
 }
diff --git a/pkg/apm/key_transactions.go b/pkg/apm/key_transactions.go
index b7212d6..2b3e074 100644
--- a/pkg/apm/key_transactions.go
+++ b/pkg/apm/key_transactions.go
@@ -2,26 +2,23 @@ package apm
 
 import (
 	"fmt"
-
-	"github.com/newrelic/newrelic-client-go/internal/utils"
 )
 
 // ListKeyTransactionsParams represents a set of filters to be
 // used when querying New Relic key transactions.
 type ListKeyTransactionsParams struct {
-	Name string
-	IDs  []int
+	Name string `url:"filter[name],omitempty"`
+	IDs  []int  `url:"filter[ids],omitempty,comma"`
 }
 
 // ListKeyTransactions returns all key transactions for an account.
 func (apm *APM) ListKeyTransactions(params *ListKeyTransactionsParams) ([]*KeyTransaction, error) {
 	response := keyTransactionsResponse{}
 	results := []*KeyTransaction{}
-	paramsMap := buildListKeyTransactionsParamsMap(params)
 	nextURL := "/key_transactions.json"
 
 	for nextURL != "" {
-		resp, err := apm.client.Get(nextURL, &paramsMap, &response)
+		resp, err := apm.client.Get(nextURL, &params, &response)
 
 		if err != nil {
 			return nil, err
@@ -50,24 +47,6 @@ func (apm *APM) GetKeyTransaction(id int) (*KeyTransaction, error) {
 	return &response.KeyTransaction, nil
 }
 
-func buildListKeyTransactionsParamsMap(params *ListKeyTransactionsParams) map[string]string {
-	paramsMap := map[string]string{}
-
-	if params == nil {
-		return paramsMap
-	}
-
-	if params.Name != "" {
-		paramsMap["filter[name]"] = params.Name
-	}
-
-	if params.IDs != nil && len(params.IDs) > 0 {
-		paramsMap["filter[ids]"] = utils.IntArrayToString(params.IDs)
-	}
-
-	return paramsMap
-}
-
 type keyTransactionsResponse struct {
 	KeyTransactions []*KeyTransaction `json:"key_transactions,omitempty"`
 }
diff --git a/pkg/dashboards/dashboards.go b/pkg/dashboards/dashboards.go
index 5619e00..0872610 100644
--- a/pkg/dashboards/dashboards.go
+++ b/pkg/dashboards/dashboards.go
@@ -2,7 +2,6 @@ package dashboards
 
 import (
 	"fmt"
-	"strconv"
 	"time"
 
 	"github.com/newrelic/newrelic-client-go/internal/http"
@@ -28,15 +27,15 @@ func New(config config.Config) Dashboards {
 // ListDashboardsParams represents a set of filters to be
 // used when querying New Relic dashboards.
 type ListDashboardsParams struct {
-	Category      string
-	CreatedAfter  *time.Time
-	CreatedBefore *time.Time
-	Page          int
-	PerPage       int
-	Sort          string
-	Title         string
-	UpdatedAfter  *time.Time
-	UpdatedBefore *time.Time
+	Category      string     `url:"filter[category],omitempty"`
+	CreatedAfter  *time.Time `url:"filter[created_after],omitempty"`
+	CreatedBefore *time.Time `url:"filter[created_before],omitempty"`
+	Page          int        `url:"page,omitempty"`
+	PerPage       int        `url:"per_page,omitempty"`
+	Sort          string     `url:"sort,omitempty"`
+	Title         string     `url:"filter[title],omitempty"`
+	UpdatedAfter  *time.Time `url:"filter[updated_after],omitempty"`
+	UpdatedBefore *time.Time `url:"filter[updated_before],omitempty"`
 }
 
 // ListDashboards is used to retrieve New Relic dashboards.
@@ -44,10 +43,9 @@ func (dashboards *Dashboards) ListDashboards(params *ListDashboardsParams) ([]Da
 	response := dashboardsResponse{}
 	d := []Dashboard{}
 	nextURL := "/dashboards.json"
-	paramsMap := buildListDashboardsParamsMap(params)
 
 	for nextURL != "" {
-		resp, err := dashboards.client.Get(nextURL, &paramsMap, &response)
+		resp, err := dashboards.client.Get(nextURL, &params, &response)
 
 		if err != nil {
 			return nil, err
@@ -122,52 +120,6 @@ func (dashboards *Dashboards) DeleteDashboard(dashboardID int) (*Dashboard, erro
 	return &response.Dashboard, nil
 }
 
-func buildListDashboardsParamsMap(params *ListDashboardsParams) map[string]string {
-	paramsMap := map[string]string{}
-
-	if params == nil {
-		return paramsMap
-	}
-
-	if params.Title != "" {
-		paramsMap["filter[title]"] = params.Title
-	}
-
-	if params.Category != "" {
-		paramsMap["filter[category]"] = params.Category
-	}
-
-	if params.CreatedBefore != nil {
-		paramsMap["filter[created_before]"] = params.CreatedBefore.Format(time.RFC3339)
-	}
-
-	if params.CreatedAfter != nil {
-		paramsMap["filter[created_after]"] = params.CreatedAfter.Format(time.RFC3339)
-	}
-
-	if params.UpdatedBefore != nil {
-		paramsMap["filter[updated_before]"] = params.UpdatedBefore.Format(time.RFC3339)
-	}
-
-	if params.UpdatedAfter != nil {
-		paramsMap["filter[updated_after]"] = params.UpdatedAfter.Format(time.RFC3339)
-	}
-
-	if params.Sort != "" {
-		paramsMap["sort"] = params.Sort
-	}
-
-	if params.Page > 0 {
-		paramsMap["page"] = strconv.Itoa(params.Page)
-	}
-
-	if params.PerPage > 0 {
-		paramsMap["per_page"] = strconv.Itoa(params.PerPage)
-	}
-
-	return paramsMap
-}
-
 type dashboardsResponse struct {
 	Dashboards []Dashboard `json:"dashboards,omitempty"`
 }
diff --git a/pkg/plugins/components.go b/pkg/plugins/components.go
new file mode 100644
index 0000000..3eda75a
--- /dev/null
+++ b/pkg/plugins/components.go
@@ -0,0 +1,148 @@
+package plugins
+
+import (
+	"fmt"
+	"time"
+)
+
+// ListComponentsParams represents a set of filters to be
+// used when querying New Relic applications.
+type ListComponentsParams struct {
+	Name         string `url:"filter[name],omitempty"`
+	IDs          []int  `url:"filter[ids],omitempty,comma"`
+	PluginID     int    `url:"filter[plugin_id],omitempty"`
+	HealthStatus bool   `url:"health_status,omitempty"`
+}
+
+// ListComponents is used to retrieve the components associated with
+// a New Relic account.
+func (p *Plugins) ListComponents(params *ListComponentsParams) ([]*Component, error) {
+	response := componentsResponse{}
+	c := []*Component{}
+	nextURL := "/components.json"
+
+	for nextURL != "" {
+		resp, err := p.client.Get(nextURL, &params, &response)
+
+		if err != nil {
+			return nil, err
+		}
+
+		c = append(c, response.Components...)
+
+		paging := p.pager.Parse(resp)
+		nextURL = paging.Next
+	}
+
+	return c, nil
+
+}
+
+// GetComponent is used to retrieve a specific New Relic component.
+func (p *Plugins) GetComponent(componentID int) (*Component, error) {
+	response := componentResponse{}
+	url := fmt.Sprintf("/components/%d.json", componentID)
+
+	_, err := p.client.Get(url, nil, &response)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Component, nil
+}
+
+// ListComponentMetricsParams represents a set of parameters to be
+// used when querying New Relic component metrics.
+type ListComponentMetricsParams struct {
+	// Name allows for filtering the returned list of metrics by name.
+	Name string `url:"name,omitempty"`
+}
+
+// ListComponentMetrics is used to retrieve the metrics for a specific New Relic component.
+func (p *Plugins) ListComponentMetrics(componentID int, params *ListComponentMetricsParams) ([]*ComponentMetric, error) {
+	m := []*ComponentMetric{}
+	response := componentMetricsResponse{}
+	nextURL := fmt.Sprintf("/components/%d/metrics.json", componentID)
+
+	for nextURL != "" {
+		resp, err := p.client.Get(nextURL, &params, &response)
+
+		if err != nil {
+			return nil, err
+		}
+
+		m = append(m, response.Metrics...)
+
+		paging := p.pager.Parse(resp)
+		nextURL = paging.Next
+	}
+
+	return m, nil
+}
+
+// GetComponentMetricDataParams represents a set of parameters to be
+// used when querying New Relic component metric data.
+type GetComponentMetricDataParams struct {
+	// Names allows retrieval of specific metrics by name.
+	// At least one metric name is required.
+	Names []string `url:"names[],omitempty"`
+
+	// Values allows retrieval of specific metric values.
+	Values []string `url:"values[],omitempty"`
+
+	// From specifies a begin time for the query.
+	From *time.Time `url:"from,omitempty"`
+
+	// To specifies an end time for the query.
+	To *time.Time `url:"to,omitempty"`
+
+	// Period represents the period of timeslices in seconds.
+	Period int `url:"period,omitempty"`
+
+	// Summarize will summarize the data when set to true.
+	Summarize bool `url:"summarize,omitempty"`
+
+	// Raw will return unformatted raw values when set to true.
+	Raw bool `url:"raw,omitempty"`
+}
+
+// GetComponentMetricData is used to retrieve the metric timeslice data for a specific component metric.
+func (p *Plugins) GetComponentMetricData(componentID int, params *GetComponentMetricDataParams) ([]*Metric, error) {
+	m := []*Metric{}
+	response := componentMetricDataResponse{}
+	nextURL := fmt.Sprintf("/components/%d/metrics/data.json", componentID)
+
+	for nextURL != "" {
+		resp, err := p.client.Get(nextURL, &params, &response)
+
+		if err != nil {
+			return nil, err
+		}
+
+		m = append(m, response.MetricData.Metrics...)
+
+		paging := p.pager.Parse(resp)
+		nextURL = paging.Next
+	}
+
+	return m, nil
+}
+
+type componentsResponse struct {
+	Components []*Component `json:"components,omitempty"`
+}
+
+type componentResponse struct {
+	Component Component `json:"component,omitempty"`
+}
+
+type componentMetricsResponse struct {
+	Metrics []*ComponentMetric `json:"metrics,omitempty"`
+}
+
+type componentMetricDataResponse struct {
+	MetricData struct {
+		Metrics []*Metric `json:"metrics,omitempty"`
+	} `json:"metric_data,omitempty"`
+}
diff --git a/pkg/plugins/components_types.go b/pkg/plugins/components_types.go
new file mode 100644
index 0000000..dd339cf
--- /dev/null
+++ b/pkg/plugins/components_types.go
@@ -0,0 +1,52 @@
+package plugins
+
+import "time"
+
+// Component represents information about a New Relic Plugins component.
+type Component struct {
+	ID             int             `json:"id"`
+	Name           string          `json:"name,omitempty"`
+	HealthStatus   string          `json:"health_status,omitempty"`
+	SummaryMetrics []SummaryMetric `json:"summary_metrics"`
+}
+
+// ComponentMetric represents metric information for a specific component.
+type ComponentMetric struct {
+	Name   string   `json:"name,omitempty"`
+	Values []string `json:"values"`
+}
+
+// SummaryMetric represents summary information for a specific metric.
+type SummaryMetric struct {
+	ID            int             `json:"id"`
+	Name          string          `json:"name"`
+	Metric        string          `json:"metric"`
+	ValueFunction string          `json:"value_function"`
+	Thresholds    MetricThreshold `json:"thresholds"`
+	Values        MetricValue     `json:"values"`
+}
+
+// MetricThreshold represents a threshold value for a metric.
+type MetricThreshold struct {
+	Caution  float64 `json:"caution"`
+	Critical float64 `json:"critical"`
+}
+
+// MetricValue represents the observed value of a metric.
+type MetricValue struct {
+	Raw       float64 `json:"raw"`
+	Formatted string  `json:"formatted"`
+}
+
+// Metric represents data for a specific metric.
+type Metric struct {
+	Name       string            `json:"name"`
+	Timeslices []MetricTimeslice `json:"timeslices"`
+}
+
+// MetricTimeslice represents the values of a metric over a given time.
+type MetricTimeslice struct {
+	From   *time.Time         `json:"from,omitempty"`
+	To     *time.Time         `json:"to,omitempty"`
+	Values map[string]float64 `json:"values,omitempty"`
+}
diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go
new file mode 100644
index 0000000..941a95c
--- /dev/null
+++ b/pkg/plugins/plugins.go
@@ -0,0 +1,22 @@
+package plugins
+
+import (
+	"github.com/newrelic/newrelic-client-go/internal/http"
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+)
+
+// Plugins is used to communicate with the New Relic Plugins product.
+type Plugins struct {
+	client http.NewRelicClient
+	pager  http.Pager
+}
+
+// New is used to create a new APM client instance.
+func New(config config.Config) Plugins {
+	pkg := Plugins{
+		client: http.NewClient(config),
+		pager:  &http.LinkHeaderPager{},
+	}
+
+	return pkg
+}
diff --git a/pkg/synthetics/monitors.go b/pkg/synthetics/monitors.go
index de1394a..80cb0df 100644
--- a/pkg/synthetics/monitors.go
+++ b/pkg/synthetics/monitors.go
@@ -3,25 +3,21 @@ package synthetics
 import (
 	"fmt"
 	"path"
-	"strconv"
 )
 
 const (
 	listMonitorsLimit = 100
 )
 
-type listMonitorsResponse struct {
-	Monitors []Monitor `json:"monitors,omitempty"`
-}
-
 // ListMonitors is used to retrieve New Relic Synthetics monitors.
 func (s *Synthetics) ListMonitors() ([]Monitor, error) {
 	resp := listMonitorsResponse{}
-	paramsMap := map[string]string{
-		"limit": strconv.Itoa(listMonitorsLimit),
+
+	queryParams := listMonitorsParams{
+		Limit: listMonitorsLimit,
 	}
 
-	_, err := s.client.Get("/monitors", &paramsMap, &resp)
+	_, err := s.client.Get("/monitors", &queryParams, &resp)
 
 	if err != nil {
 		return nil, err
@@ -81,3 +77,11 @@ func (s *Synthetics) DeleteMonitor(monitorID string) error {
 
 	return nil
 }
+
+type listMonitorsResponse struct {
+	Monitors []Monitor `json:"monitors,omitempty"`
+}
+
+type listMonitorsParams struct {
+	Limit int `url:"limit,omitempty"`
+}


Test patch:
diff --git a/internal/http/client_test.go b/internal/http/client_test.go
index 08323f5..854d38b 100644
--- a/internal/http/client_test.go
+++ b/internal/http/client_test.go
@@ -97,9 +97,12 @@ func TestResponseValue(t *testing.T) {
 
 func TestQueryParams(t *testing.T) {
 	t.Parallel()
-	queryParams := map[string]string{
-		"a": "1",
-		"b": "2",
+	queryParams := struct {
+		A int `url:"a,omitempty"`
+		B int `url:"b,omitempty"`
+	}{
+		A: 1,
+		B: 2,
 	}
 
 	c := NewTestAPIClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -254,7 +257,7 @@ func TestPost(t *testing.T) {
 		_, _ = w.Write([]byte(`{}`))
 	}))
 
-	_, err := c.Post("/path", &map[string]string{}, &struct{}{}, &struct{}{})
+	_, err := c.Post("/path", &struct{}{}, &struct{}{}, &struct{}{})
 
 	assert.NoError(t, err)
 }
@@ -266,7 +269,7 @@ func TestPut(t *testing.T) {
 		_, _ = w.Write([]byte(`{}`))
 	}))
 
-	_, err := c.Put("/path", &map[string]string{}, &struct{}{}, &struct{}{})
+	_, err := c.Put("/path", &struct{}{}, &struct{}{}, &struct{}{})
 
 	assert.NoError(t, err)
 }
@@ -279,7 +282,7 @@ func TestDelete(t *testing.T) {
 		_, _ = w.Write([]byte(`{}`))
 	}))
 
-	_, err := c.Delete("/path", &map[string]string{}, &struct{}{})
+	_, err := c.Delete("/path", &struct{}{}, &struct{}{})
 
 	assert.NoError(t, err)
 }
diff --git a/pkg/apm/applications_test.go b/pkg/apm/applications_test.go
index 661a677..d9b9538 100644
--- a/pkg/apm/applications_test.go
+++ b/pkg/apm/applications_test.go
@@ -183,10 +183,10 @@ func TestListApplicationsWithParams(t *testing.T) {
 	}))
 
 	params := ListApplicationsParams{
-		Name:     &expectedName,
-		Host:     &expectedHost,
+		Name:     expectedName,
+		Host:     expectedHost,
 		IDs:      []int{123, 456},
-		Language: &expectedLanguage,
+		Language: expectedLanguage,
 	}
 
 	_, err := apm.ListApplications(&params)
diff --git a/pkg/apm/deployments_integration_test.go b/pkg/apm/deployments_integration_test.go
index 6b7eb82..0da84b9 100644
--- a/pkg/apm/deployments_integration_test.go
+++ b/pkg/apm/deployments_integration_test.go
@@ -16,7 +16,7 @@ func TestIntegrationDeployments(t *testing.T) {
 	// Setup
 	appFilterName := "tf_" // Filter for integration test applications
 	appQueryParams := ListApplicationsParams{
-		Name: &appFilterName,
+		Name: appFilterName,
 	}
 	applications, err := client.ListApplications(&appQueryParams)
 
diff --git a/pkg/apm/labels_integration_test.go b/pkg/apm/labels_integration_test.go
index e25ee19..0f43743 100644
--- a/pkg/apm/labels_integration_test.go
+++ b/pkg/apm/labels_integration_test.go
@@ -18,7 +18,7 @@ func TestIntegrationLabels(t *testing.T) {
 	// Setup
 	appFilterName := "tf_" // Filter for integration test applications
 	appQueryParams := ListApplicationsParams{
-		Name: &appFilterName,
+		Name: appFilterName,
 	}
 	applications, err := client.ListApplications(&appQueryParams)
 
diff --git a/pkg/plugins/components_integration_test.go b/pkg/plugins/components_integration_test.go
new file mode 100644
index 0000000..76e6c60
--- /dev/null
+++ b/pkg/plugins/components_integration_test.go
@@ -0,0 +1,46 @@
+// +build integration
+
+package plugins
+
+import (
+	"os"
+	"testing"
+
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIntegrationComponents(t *testing.T) {
+	t.Parallel()
+
+	apiKey := os.Getenv("NEWRELIC_API_KEY")
+
+	if apiKey == "" {
+		t.Skipf("acceptance testing requires an API key")
+	}
+
+	api := New(config.Config{
+		APIKey: apiKey,
+	})
+
+	a, err := api.ListComponents(nil)
+
+	require.NoError(t, err)
+
+	c, err := api.GetComponent(a[0].ID)
+
+	require.NoError(t, err)
+	require.NotNil(t, c)
+
+	m, err := api.ListComponentMetrics(c.ID, nil)
+
+	require.NoError(t, err)
+	require.NotNil(t, m)
+
+	params := GetComponentMetricDataParams{
+		Names: []string{m[0].Name},
+	}
+	_, err = api.GetComponentMetricData(a[0].ID, &params)
+
+	require.NoError(t, err)
+}
diff --git a/pkg/plugins/components_test.go b/pkg/plugins/components_test.go
new file mode 100644
index 0000000..b9fa539
--- /dev/null
+++ b/pkg/plugins/components_test.go
@@ -0,0 +1,276 @@
+// +build unit
+
+package plugins
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	testTimestamp, _ = time.Parse(time.RFC3339, time.RFC3339)
+
+	testComponent = Component{
+		ID:   212222915,
+		Name: "Redis",
+		SummaryMetrics: []SummaryMetric{
+			{
+				ID:            190174,
+				Name:          "Connected Clients",
+				Metric:        "Component/Connection/Clients[connections]",
+				ValueFunction: "average_value",
+				Thresholds: MetricThreshold{
+					Caution:  3,
+					Critical: 0,
+				},
+			},
+		},
+	}
+
+	testComponentJSON = `{
+		"id": 212222915,
+		"name": "Redis",
+		"summary_metrics": [
+			{
+				"id": 190174,
+				"name": "Connected Clients",
+				"metric": "Component/Connection/Clients[connections]",
+				"value_function": "average_value",
+				"thresholds": {
+					"caution": 3,
+					"critical": 0
+				}
+			}
+		]
+	}`
+
+	testComponentMetric = ComponentMetric{
+		Name: "Component/Memory/RSS[bytes]",
+		Values: []string{
+			"average_value",
+			"total_value",
+			"max_value",
+			"min_value",
+			"standard_deviation",
+			"rate",
+			"count",
+		},
+	}
+
+	testComponentMetricJSON = `{
+		"name": "Component/Memory/RSS[bytes]",
+		"values": [
+			"average_value",
+			"total_value",
+			"max_value",
+			"min_value",
+			"standard_deviation",
+			"rate",
+			"count"
+		]
+	}`
+
+	testMetricData = Metric{
+		Name: "Component/Memory/RSS[bytes]",
+		Timeslices: []MetricTimeslice{
+			{
+				From: &testTimestamp,
+				To:   &testTimestamp,
+				Values: map[string]float64{
+					"average_value":      10,
+					"total_value":        10,
+					"max_value":          10,
+					"min_value":          10,
+					"standard_deviation": 10,
+					"rate":               10,
+					"count":              10,
+				},
+			},
+		},
+	}
+
+	testMetricDataJSON = fmt.Sprintf(`{
+        "name": "Component/Memory/RSS[bytes]",
+        "timeslices": [
+			{
+				"from": "%[1]s",
+				"to": "%[1]s",
+				"values": {
+					"average_value": 10,
+					"total_value": 10,
+					"max_value": 10,
+					"min_value": 10,
+					"standard_deviation": 10,
+					"rate": 10,
+					"count": 10
+				}
+			}
+		]
+	}`, testTimestamp.Format(time.RFC3339))
+)
+
+func TestListComponents(t *testing.T) {
+	t.Parallel()
+	responseJSON := fmt.Sprintf(`{"components": [%s]}`, testComponentJSON)
+	apm := newMockResponse(t, responseJSON, http.StatusOK)
+	c, err := apm.ListComponents(nil)
+
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, &testComponent, c[0])
+}
+
+func TestListComponentsWithParams(t *testing.T) {
+	t.Parallel()
+	expectedName := "componentName"
+	expectedIDs := "123,456"
+	expectedPluginID := "1234"
+	expectedHealthStatus := "true"
+
+	apm := newTestPluginsClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		values := r.URL.Query()
+
+		name := values.Get("filter[name]")
+		if name != expectedName {
+			t.Errorf(`expected name filter "%s", received: "%s"`, expectedName, name)
+		}
+
+		ids := values.Get("filter[ids]")
+		if ids != expectedIDs {
+			t.Errorf(`expected ID filter "%s", received: "%s"`, expectedIDs, ids)
+		}
+
+		pluginID := values.Get("filter[plugin_id]")
+		if pluginID != expectedPluginID {
+			t.Errorf(`expected plugin ID filter "%s", received: "%s"`, expectedPluginID, pluginID)
+		}
+
+		healthStatus := values.Get("health_status")
+		if healthStatus != expectedHealthStatus {
+			t.Errorf(`expected health status filter "%s", received: "%s"`, expectedHealthStatus, healthStatus)
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		_, err := w.Write([]byte(`{"applications":[]}`))
+
+		require.NoError(t, err)
+	}))
+
+	params := ListComponentsParams{
+		IDs:          []int{123, 456},
+		PluginID:     1234,
+		Name:         expectedName,
+		HealthStatus: true,
+	}
+
+	c, err := apm.ListComponents(&params)
+
+	require.NoError(t, err)
+	require.NotNil(t, c)
+}
+
+func TestGetComponent(t *testing.T) {
+	responseJSON := fmt.Sprintf(`{"component": %s}`, testComponentJSON)
+	apm := newMockResponse(t, responseJSON, http.StatusOK)
+	c, err := apm.GetComponent(testComponent.ID)
+
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, testComponent, *c)
+}
+
+func TestListComponentMetrics(t *testing.T) {
+	responseJSON := fmt.Sprintf(`{"metrics": [%s]}`, testComponentMetricJSON)
+	apm := newMockResponse(t, responseJSON, http.StatusOK)
+
+	m, err := apm.ListComponentMetrics(testComponent.ID, nil)
+
+	require.NoError(t, err)
+	require.NotNil(t, m)
+	require.Equal(t, &testComponentMetric, m[0])
+}
+
+func TestGetComponentMetricData(t *testing.T) {
+	responseJSON := fmt.Sprintf(`{
+		"metric_data": {
+			 "metrics": [%s]
+		}
+	}`, testMetricDataJSON)
+	apm := newMockResponse(t, responseJSON, http.StatusOK)
+	m, err := apm.GetComponentMetricData(testComponent.ID, nil)
+
+	require.NoError(t, err)
+	require.NotNil(t, m)
+	require.Equal(t, &testMetricData, m[0])
+}
+
+func TestGetComponentMetricDataWithParams(t *testing.T) {
+	expectedNames := "componentName"
+	expectedValues := "123"
+	expectedTo := testTimestamp.Format(time.RFC3339)
+	expectedFrom := testTimestamp.Format(time.RFC3339)
+	expectedPeriod := "30"
+	expectedSummarize := "true"
+	expectedRaw := "true"
+
+	apm := newTestPluginsClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		values := r.URL.Query()
+
+		names := values.Get("names[]")
+		if names != expectedNames {
+			t.Errorf(`expected names filter "%s", received: "%s"`, expectedNames, names)
+		}
+
+		v := values.Get("values[]")
+		if v != expectedValues {
+			t.Errorf(`expected values filter "%s", received: "%s"`, expectedValues, v)
+		}
+
+		from := values.Get("from")
+		if from != expectedFrom {
+			t.Errorf(`expected from param "%s", received: "%s"`, expectedFrom, from)
+		}
+
+		to := values.Get("to")
+		if to != expectedTo {
+			t.Errorf(`expected to param "%s", received: "%s"`, expectedTo, to)
+		}
+
+		period := values.Get("period")
+		if period != expectedPeriod {
+			t.Errorf(`expected period param "%s", received: "%s"`, expectedPeriod, period)
+		}
+
+		raw := values.Get("raw")
+		if raw != expectedRaw {
+			t.Errorf(`expected raw param "%s", received: "%s"`, expectedRaw, raw)
+		}
+
+		summarize := values.Get("summarize")
+		if summarize != expectedSummarize {
+			t.Errorf(`expected summarize param "%s", received: "%s"`, expectedSummarize, summarize)
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		_, err := w.Write([]byte(`{"applications":[]}`))
+
+		require.NoError(t, err)
+	}))
+
+	params := GetComponentMetricDataParams{
+		Names:     []string{"componentName"},
+		Values:    []string{"123"},
+		From:      &testTimestamp,
+		To:        &testTimestamp,
+		Period:    30,
+		Summarize: true,
+		Raw:       true,
+	}
+	_, err := apm.GetComponentMetricData(testComponent.ID, &params)
+
+	require.NoError(t, err)
+}
diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go
new file mode 100644
index 0000000..c98ad79
--- /dev/null
+++ b/pkg/plugins/plugins_test.go
@@ -0,0 +1,53 @@
+package plugins
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+	"github.com/stretchr/testify/require"
+)
+
+// nolint
+func newTestPluginsClient(handler http.Handler) Plugins {
+	ts := httptest.NewServer(handler)
+
+	c := New(config.Config{
+		APIKey:    "abc123",
+		BaseURL:   ts.URL,
+		UserAgent: "newrelic/newrelic-client-go",
+	})
+
+	return c
+}
+
+// nolint
+func newMockResponse(
+	t *testing.T,
+	mockJSONResponse string,
+	statusCode int,
+) Plugins {
+	return newTestPluginsClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(statusCode)
+
+		_, err := w.Write([]byte(mockJSONResponse))
+
+		require.NoError(t, err)
+	}))
+}
+
+// nolint
+func newIntegrationTestClient(t *testing.T) Plugins {
+	apiKey := os.Getenv("NEWRELIC_API_KEY")
+
+	if apiKey == "" {
+		t.Skipf("acceptance testing requires an API key")
+	}
+
+	return New(config.Config{
+		APIKey: apiKey,
+	})
+}