satellite/console: Add endpoint for clientside analytics events
This is a very simple endpoint which allows the satellite UI client to notify the console server that an event has occurred. We will use this to track when users have completed certain tasks that can't be tracked server-side (e.g. generating gateway credentials, setting a passphrase) As part of this change, one client side event is implemented to use the endpoint - when the user clicks the button to create gateway credentials after making a new access grant. Change-Id: Ic8fa729f1c84474788e1de84c18532aef8e8fa3c
This commit is contained in:
parent
4330a4bc4b
commit
7e4e1040f2
@ -11,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
eventAccountCreated = "Account Created"
|
||||
eventAccountCreated = "Account Created"
|
||||
gatewayCredentialsCreated = "Credentials Created"
|
||||
)
|
||||
|
||||
// Config is a configuration struct for analytics Service.
|
||||
@ -27,6 +28,7 @@ type Service struct {
|
||||
log *zap.Logger
|
||||
config Config
|
||||
satelliteName string
|
||||
clientEvents map[string]bool
|
||||
|
||||
segment segment.Client
|
||||
}
|
||||
@ -37,10 +39,14 @@ func NewService(log *zap.Logger, config Config, satelliteName string) *Service {
|
||||
log: log,
|
||||
config: config,
|
||||
satelliteName: satelliteName,
|
||||
clientEvents: make(map[string]bool),
|
||||
}
|
||||
if config.Enabled {
|
||||
service.segment = segment.New(config.SegmentWriteKey)
|
||||
}
|
||||
for _, name := range []string{gatewayCredentialsCreated} {
|
||||
service.clientEvents[name] = true
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
@ -114,3 +120,18 @@ func (service *Service) TrackCreateUser(fields TrackCreateUserFields) {
|
||||
Properties: props,
|
||||
})
|
||||
}
|
||||
|
||||
// TrackEvent sends an arbitrary event associated with user ID to Segment.
|
||||
// It is used for tracking occurrences of client-side events.
|
||||
func (service *Service) TrackEvent(eventName string, userID uuid.UUID) {
|
||||
// do not track if the event name is an invalid client-side event
|
||||
if !service.clientEvents[eventName] {
|
||||
service.log.Error("Invalid client-triggered event", zap.String("eventName", eventName))
|
||||
return
|
||||
}
|
||||
|
||||
service.enqueueMessage(segment.Track{
|
||||
UserId: userID.String(),
|
||||
Event: eventName,
|
||||
})
|
||||
}
|
||||
|
92
satellite/console/consoleweb/consoleapi/analytics.go
Normal file
92
satellite/console/consoleweb/consoleapi/analytics.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/storj/satellite/analytics"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAnalyticsAPI - console analytics api error type.
|
||||
ErrAnalyticsAPI = errs.Class("console analytics api error")
|
||||
)
|
||||
|
||||
// Analytics is an api controller that exposes analytics related functionality.
|
||||
type Analytics struct {
|
||||
log *zap.Logger
|
||||
service *console.Service
|
||||
analytics *analytics.Service
|
||||
}
|
||||
|
||||
// NewAnalytics is a constructor for api analytics controller.
|
||||
func NewAnalytics(log *zap.Logger, service *console.Service, a *analytics.Service) *Analytics {
|
||||
return &Analytics{
|
||||
log: log,
|
||||
service: service,
|
||||
analytics: a,
|
||||
}
|
||||
}
|
||||
|
||||
type eventTriggeredBody struct {
|
||||
EventName string `json:"eventName"`
|
||||
}
|
||||
|
||||
// EventTriggered tracks the occurrence of an arbitrary event on the client.
|
||||
func (a *Analytics) EventTriggered(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
}
|
||||
var et eventTriggeredBody
|
||||
err = json.Unmarshal(body, &et)
|
||||
if err != nil {
|
||||
a.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
}
|
||||
userID, err := a.service.GetUserID(ctx)
|
||||
if err != nil {
|
||||
a.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.analytics.TrackEvent(et.EventName, userID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// serveJSONError writes JSON error to response output stream.
|
||||
func (a *Analytics) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
if status == http.StatusNoContent {
|
||||
return
|
||||
}
|
||||
|
||||
if status == http.StatusInternalServerError {
|
||||
a.log.Error("returning internal server error to client", zap.Int("code", status), zap.Error(err))
|
||||
} else {
|
||||
a.log.Debug("returning error to client", zap.Int("code", status), zap.Error(err))
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
response.Error = err.Error()
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
if err != nil {
|
||||
a.log.Error("failed to write json error response", zap.Error(ErrAPIKeysAPI.Wrap(err)))
|
||||
}
|
||||
}
|
@ -211,6 +211,11 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
apiKeysRouter.Use(server.withAuth)
|
||||
apiKeysRouter.HandleFunc("/delete-by-name", apiKeysController.DeleteByNameAndProjectID).Methods(http.MethodDelete)
|
||||
|
||||
analyticsController := consoleapi.NewAnalytics(logger, service, server.analytics)
|
||||
analyticsRouter := router.PathPrefix("/api/v0/analytics").Subrouter()
|
||||
analyticsRouter.Use(server.withAuth)
|
||||
analyticsRouter.HandleFunc("/event", analyticsController.EventTriggered).Methods(http.MethodPost)
|
||||
|
||||
if server.config.StaticDir != "" {
|
||||
router.HandleFunc("/activation/", server.accountActivationHandler)
|
||||
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)
|
||||
|
@ -795,6 +795,17 @@ func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (u *User, err error
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserID returns the User ID from the session.
|
||||
func (s *Service) GetUserID(ctx context.Context) (id uuid.UUID, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
auth, err := s.getAuthAndAuditLog(ctx, "get user ID")
|
||||
if err != nil {
|
||||
return uuid.UUID{}, Error.Wrap(err)
|
||||
}
|
||||
return auth.User.ID, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail returns User by email.
|
||||
func (s *Service) GetUserByEmail(ctx context.Context, email string) (u *User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
32
web/satellite/src/api/analytics.ts
Normal file
32
web/satellite/src/api/analytics.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
|
||||
/**
|
||||
* AnalyticsHttpApi is a console Analytics API.
|
||||
* Exposes all analytics-related functionality
|
||||
*/
|
||||
export class AnalyticsHttpApi {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/analytics';
|
||||
|
||||
/**
|
||||
* Used to get authentication token.
|
||||
*
|
||||
* @param eventName - name of the event
|
||||
* @throws Error
|
||||
*/
|
||||
public async eventTriggered(eventName: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/event`;
|
||||
const body = {
|
||||
eventName: eventName,
|
||||
};
|
||||
const response = await this.http.post(path, JSON.stringify(body));
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Can not track event');
|
||||
}
|
||||
}
|
@ -104,6 +104,8 @@ import HideIcon from '@/../static/images/common/BlackArrowHide.svg';
|
||||
import { RouteConfig } from '@/router';
|
||||
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
|
||||
import { GatewayCredentials } from '@/types/accessGrants';
|
||||
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -120,6 +122,7 @@ export default class GatewayStep extends Vue {
|
||||
private key: string = '';
|
||||
private restrictedKey: string = '';
|
||||
private access: string = '';
|
||||
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
|
||||
|
||||
public areKeysVisible: boolean = false;
|
||||
public isLoading: boolean = false;
|
||||
@ -217,6 +220,8 @@ export default class GatewayStep extends Vue {
|
||||
|
||||
await this.$notify.success('Gateway credentials were generated successfully');
|
||||
|
||||
await this.analytics.eventTriggered(AnalyticsEvent.GATEWAY_CREDENTIALS_CREATED);
|
||||
|
||||
this.areKeysVisible = true;
|
||||
} catch (error) {
|
||||
await this.$notify.error(error.message);
|
||||
|
@ -21,3 +21,8 @@ export enum SegmentEvent {
|
||||
CLI_DOCS_VIEWED = 'Uplink CLI Docs Viewed',
|
||||
GENERATE_GATEWAY_CREDENTIALS_CLICKED = 'Generate Gateway Credentials Clicked',
|
||||
}
|
||||
|
||||
// Make sure these event names match up with the client-side event names in satellite/analytics/service.go
|
||||
export enum AnalyticsEvent {
|
||||
GATEWAY_CREDENTIALS_CREATED = 'Credentials Created',
|
||||
}
|
Loading…
Reference in New Issue
Block a user