Create a Custom Deletion Requestor¶
When a user runs telemetry reset, the framework sends a data deletion request to the remote backend. GTB ships three built-in requestors (HTTP, email, event-based), but your backend may require a different mechanism โ a GraphQL mutation, a queue message, or a vendor-specific API call.
This guide walks through:
- Implementing
telemetry.DeletionRequestor - Wiring the factory into
TelemetryConfig - Testing the requestor
- User-facing behaviour
Step 1: Implement telemetry.DeletionRequestor¶
The interface has a single method:
Key requirements:
machineIDis the SHA-256-derived anonymised identifier (16 hex chars). This is the only identifier available for deletion โ it's what appears in every telemetry event.- Return an error if the deletion request fails โ the framework will inform the user and suggest contacting the help channel.
- Deletion is best-effort. Not all backends can guarantee deletion (e.g. append-only logs). The requestor should make a reasonable attempt.
Create a requestor, e.g. for a GraphQL API:
package myanalytics
import (
"bytes"
"context"
"encoding/json"
"net/http"
"time"
"github.com/cockroachdb/errors"
gtbhttp "gitlab.com/phpboyscout/go-tool-base/pkg/http"
"gitlab.com/phpboyscout/go-tool-base/pkg/logger"
"gitlab.com/phpboyscout/go-tool-base/pkg/telemetry"
)
const requestTimeout = 10 * time.Second
type graphQLRequestor struct {
endpoint string
apiKey string
client *http.Client
log logger.Logger
}
// NewDeletionRequestor creates a requestor that sends a GraphQL mutation
// to delete telemetry data for the given machine ID.
func NewDeletionRequestor(endpoint, apiKey string, log logger.Logger) telemetry.DeletionRequestor {
return &graphQLRequestor{
endpoint: endpoint,
apiKey: apiKey,
client: gtbhttp.NewClient(gtbhttp.WithTimeout(requestTimeout)),
log: log,
}
}
Step 2: Implement RequestDeletion¶
type graphQLRequest struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
func (r *graphQLRequestor) RequestDeletion(ctx context.Context, machineID string) error {
payload := graphQLRequest{
Query: `mutation DeleteTelemetry($machineId: String!) {
deleteTelemetryData(machineId: $machineId) {
success
deletedCount
}
}`,
Variables: map[string]any{
"machineId": machineID,
},
}
body, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "marshalling deletion request")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.endpoint, bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "creating deletion request")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+r.apiKey)
resp, err := r.client.Do(req)
if err != nil {
return errors.Wrap(err, "sending deletion request")
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= 400 {
r.log.Debug("deletion endpoint returned non-success status",
"status", resp.StatusCode, "endpoint", r.endpoint)
return errors.Newf("deletion request returned status %d", resp.StatusCode)
}
return nil
}
Step 3: Wire It Into Your Tool¶
Use TelemetryConfig.DeletionRequestor to supply a factory function. Like the backend factory, it returns any to avoid import cycles. The returned value must implement telemetry.DeletionRequestor.
import "myorg/pkg/myanalytics"
p := &props.Props{
Tool: props.Tool{
Name: "mytool",
Features: props.SetFeatures(
props.Enable(props.TelemetryCmd),
),
Telemetry: props.TelemetryConfig{
Endpoint: "https://analytics.example.com/events",
DeletionRequestor: func(p *props.Props) any {
return myanalytics.NewDeletionRequestor(
"https://analytics.example.com/graphql",
p.Config.GetString("analytics.api_key"),
p.Logger,
)
},
},
},
}
Fallback behaviour
If no DeletionRequestor is configured, or if the factory returns a value that doesn't implement the interface, the framework falls back to sending a data.deletion_request event through the existing telemetry backend. This works with any backend type but relies on server-side processing of the event.
Step 4: Write Tests¶
func TestGraphQLRequestor_Success(t *testing.T) {
t.Parallel()
var receivedBody graphQLRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &receivedBody)
if r.Header.Get("Authorization") == "" {
t.Error("missing Authorization header")
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"data":{"deleteTelemetryData":{"success":true}}}`))
}))
defer srv.Close()
r := NewDeletionRequestor(srv.URL, "test-key", logger.NewNoop())
err := r.RequestDeletion(context.Background(), "abc123def456")
if err != nil {
t.Fatalf("deletion error: %v", err)
}
if receivedBody.Variables["machineId"] != "abc123def456" {
t.Errorf("machineId = %v, want abc123def456", receivedBody.Variables["machineId"])
}
}
func TestGraphQLRequestor_ServerError(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
r := NewDeletionRequestor(srv.URL, "test-key", logger.NewNoop())
err := r.RequestDeletion(context.Background(), "abc123")
if err == nil {
t.Error("expected error for 500 response")
}
}
Built-in Requestors¶
For reference, the framework provides three built-in requestors that cover common patterns:
| Requestor | Use case | Constructor |
|---|---|---|
| HTTP | REST API with POST {"machine_id": "..."} |
telemetry.NewHTTPDeletionRequestor(endpoint, logger) |
Opens a pre-filled mailto: link for the user |
telemetry.NewEmailDeletionRequestor(address, toolName) |
|
| Event | Sends a data.deletion_request event through the backend |
telemetry.NewEventDeletionRequestor(backend) |
You can use these directly instead of writing a custom requestor:
Telemetry: props.TelemetryConfig{
DeletionRequestor: func(p *props.Props) any {
return telemetry.NewHTTPDeletionRequestor(
"https://analytics.example.com/delete",
p.Logger,
)
},
},
User-Facing Behaviour¶
When the user runs telemetry reset:
- All local data (buffer, spill files, local log) is cleared immediately
- Your
RequestDeletionis called with the machine ID - If it succeeds:
"Deletion request sent for machine ID: 4a3f8c1d9e2b6f70" - If it fails: the error is shown and the user is directed to the help channel (if configured)
- Telemetry is disabled regardless of deletion outcome
$ mytool telemetry reset
Deletion request sent for machine ID: 4a3f8c1d9e2b6f70
Local telemetry data cleared. Telemetry disabled.
Related Documentation¶
- Telemetry Component โ architecture, GDPR deletion, privacy controls
- Create a Custom Telemetry Backend โ the
Backendinterface - Telemetry Command โ the
resetcommand