Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion ably/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1194,9 +1194,18 @@ func WithLogLevel(level LogLevel) ClientOption {

// WithAgents is used to add product/version key-value pairs to include in the
// agent library identifiers. This must only be used by Ably-authored SDKs.
// Successive calls merge their entries into any agents already configured
// rather than replacing them.
func WithAgents(agents map[string]string) ClientOption {
return func(os *clientOptions) {
os.Agents = agents
merged := make(map[string]string, len(os.Agents)+len(agents))
for product, version := range os.Agents {
merged[product] = version
}
for product, version := range agents {
merged[product] = version
}
os.Agents = merged
}
}

Expand Down
21 changes: 21 additions & 0 deletions pubsub/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package device provides the Ably Pub/Sub client for devices: applications
// running on end-user devices, whose connections are identified by a
// clientId and counted on accounts with monthly-active-user billing.
package device

import "github.com/ably/ably-go/ably"

// agentName declares the side in the Ably-Agent header (RSC7d) so that
// traffic from clients constructed by this package is classified as
// device-side.
const agentName = "ably-go-pubsub-device"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

probably should be ably-pubsub-device-go

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Based on our internal decision record, the agent entry needs to end in -device or -server.


// Client is a device Pub/Sub client.
type Client = ably.Realtime

// NewClient constructs a device Pub/Sub client: a realtime connection to
// Ably with channels, presence and history.
func NewClient(opts ...ably.ClientOption) (*Client, error) {
opts = append(opts[:len(opts):len(opts)], ably.WithAgents(map[string]string{agentName: ""}))
return ably.NewRealtime(opts...)
}
38 changes: 38 additions & 0 deletions pubsub/device/device_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package device_test

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/ably/ably-go/ably"
"github.com/ably/ably-go/pubsub/device"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewClient_DeclaresDeviceAgent(t *testing.T) {
var agent string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
agent = r.Header.Get(ably.AblyAgentHeaderName)
w.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
u, err := url.Parse(ts.URL)
require.NoError(t, err)

// The realtime client's HTTP requests carry the same agents as its
// connection, so assert the header via Time without connecting.
client, err := device.NewClient(
ably.WithEndpoint(u.Host),
ably.WithTLS(false),
ably.WithUseTokenAuth(true),
ably.WithAutoConnect(false),
)
require.NoError(t, err)

client.Time(context.Background())
assert.Equal(t, ably.AgentIdentifier(map[string]string{"ably-go-pubsub-device": ""}), agent)
}
32 changes: 32 additions & 0 deletions pubsub/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package server provides the Ably Pub/Sub clients for servers: trusted
// environments which typically authenticate with an API key and whose
// connections are exempt from monthly-active-user counting.
package server

import "github.com/ably/ably-go/ably"

// agentName declares the side in the Ably-Agent header (RSC7d) so that
// traffic from clients constructed by this package is classified as
// server-side.
const agentName = "ably-go-pubsub-server"

// HTTPClient is a server Pub/Sub client that operates entirely over HTTP.
type HTTPClient = ably.REST

// RealtimeClient is a server Pub/Sub client with a realtime connection.
type RealtimeClient = ably.Realtime

// NewHTTPClient constructs a server Pub/Sub client that operates entirely
// over HTTP: publish, history, presence reads, stats, token issuing.
func NewHTTPClient(opts ...ably.ClientOption) (*HTTPClient, error) {
opts = append(opts[:len(opts):len(opts)], ably.WithAgents(map[string]string{agentName: ""}))
return ably.NewREST(opts...)
}

// NewRealtimeClient constructs a server Pub/Sub client with a persistent
// realtime connection: everything the HTTP client does, plus subscribing
// to channels and entering presence.
func NewRealtimeClient(opts ...ably.ClientOption) (*RealtimeClient, error) {
opts = append(opts[:len(opts):len(opts)], ably.WithAgents(map[string]string{agentName: ""}))
return ably.NewRealtime(opts...)
}
56 changes: 56 additions & 0 deletions pubsub/server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package server_test

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/ably/ably-go/ably"
"github.com/ably/ably-go/pubsub/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// newAgentRecorder returns a test server which records the Ably-Agent header
// of each request it receives, along with options pointing a client at it.
func newAgentRecorder(t *testing.T) (agent *string, opts []ably.ClientOption) {
t.Helper()
agent = new(string)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*agent = r.Header.Get(ably.AblyAgentHeaderName)
w.WriteHeader(http.StatusInternalServerError)
}))
t.Cleanup(ts.Close)
u, err := url.Parse(ts.URL)
require.NoError(t, err)
return agent, []ably.ClientOption{
ably.WithEndpoint(u.Host),
ably.WithTLS(false),
ably.WithUseTokenAuth(true),
}
}

func TestNewHTTPClient_DeclaresServerAgent(t *testing.T) {
agent, opts := newAgentRecorder(t)

client, err := server.NewHTTPClient(opts...)
require.NoError(t, err)

client.Time(context.Background())
assert.Equal(t, ably.AgentIdentifier(map[string]string{"ably-go-pubsub-server": ""}), *agent)
}

func TestNewHTTPClient_MergesUserAgents(t *testing.T) {
agent, opts := newAgentRecorder(t)

client, err := server.NewHTTPClient(append(opts, ably.WithAgents(map[string]string{"foo": "1.2.3"}))...)
require.NoError(t, err)

client.Time(context.Background())
// Agent map iteration order is unspecified, so assert each entry.
assert.True(t, strings.Contains(*agent, " ably-go-pubsub-server"), "missing side agent: %q", *agent)
assert.True(t, strings.Contains(*agent, " foo/1.2.3"), "missing user agent: %q", *agent)
}
Loading