Skip to content

Commit

Permalink
chore: migrate shortcut analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
boojack committed Nov 22, 2023
1 parent c85442d commit 3be52e7
Show file tree
Hide file tree
Showing 13 changed files with 772 additions and 157 deletions.
74 changes: 74 additions & 0 deletions api/v2/shortcut_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import (
"strings"
"time"

"github.com/mssola/useragent"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"

apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
storepb "github.com/boojack/slash/proto/gen/store"
"github.com/boojack/slash/server/metric"
"github.com/boojack/slash/store"
)

Expand Down Expand Up @@ -218,6 +221,77 @@ func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv2pb.Dele
return response, nil
}

func (s *APIV2Service) GetShortcutAnalytics(ctx context.Context, request *apiv2pb.GetShortcutAnalyticsRequest) (*apiv2pb.GetShortcutAnalyticsResponse, error) {
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
ID: &request.Id,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get shortcut by name: %v", err)
}
if shortcut == nil {
return nil, status.Errorf(codes.NotFound, "shortcut not found")
}

activities, err := s.Store.ListActivities(ctx, &store.FindActivity{
Type: store.ActivityShortcutView,
Where: []string{fmt.Sprintf("json_extract(payload, '$.shortcutId') = %d", request.Id)},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get activities, err: %v", err)
}

referenceMap := make(map[string]int32)
deviceMap := make(map[string]int32)
browserMap := make(map[string]int32)
for _, activity := range activities {
payload := &storepb.ActivityShorcutViewPayload{}
if err := protojson.Unmarshal([]byte(activity.Payload), payload); err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to unmarshal payload, err: %v", err))
}

if _, ok := referenceMap[payload.Referer]; !ok {
referenceMap[payload.Referer] = 0
}
referenceMap[payload.Referer]++

ua := useragent.New(payload.UserAgent)
deviceName := ua.OSInfo().Name
browserName, _ := ua.Browser()

if _, ok := deviceMap[deviceName]; !ok {
deviceMap[deviceName] = 0
}
deviceMap[deviceName]++

if _, ok := browserMap[browserName]; !ok {
browserMap[browserName] = 0
}
browserMap[browserName]++
}

metric.Enqueue("shortcut analytics")
response := &apiv2pb.GetShortcutAnalyticsResponse{
References: mapToAnalyticsSlice(referenceMap),
Devices: mapToAnalyticsSlice(deviceMap),
Browsers: mapToAnalyticsSlice(browserMap),
}
return response, nil
}

func mapToAnalyticsSlice(m map[string]int32) []*apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem {
analyticsSlice := make([]*apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem, 0)
for key, value := range m {
analyticsSlice = append(analyticsSlice, &apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem{
Name: key,
Count: value,
})
}
slices.SortFunc(analyticsSlice, func(i, j *apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem) int {
return int(i.Count - j.Count)
})
return analyticsSlice
}

func (s *APIV2Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
payload := &storepb.ActivityShorcutCreatePayload{
ShortcutId: shortcut.Id,
Expand Down
21 changes: 11 additions & 10 deletions frontend/web/src/components/AnalyticsView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import classNames from "classnames";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as api from "../helpers/api";
import { shortcutServiceClient } from "@/grpcweb";
import { GetShortcutAnalyticsResponse } from "@/types/proto/api/v2/shortcut_service";
import Icon from "./Icon";

interface Props {
Expand All @@ -12,12 +13,12 @@ interface Props {
const AnalyticsView: React.FC<Props> = (props: Props) => {
const { shortcutId, className } = props;
const { t } = useTranslation();
const [analytics, setAnalytics] = useState<AnalysisData | null>(null);
const [analytics, setAnalytics] = useState<GetShortcutAnalyticsResponse | null>(null);
const [selectedDeviceTab, setSelectedDeviceTab] = useState<"os" | "browser">("browser");

useEffect(() => {
api.getShortcutAnalytics(shortcutId).then(({ data }) => {
setAnalytics(data);
shortcutServiceClient.getShortcutAnalytics({ id: shortcutId }).then((response) => {
setAnalytics(response);
});
}, []);

Expand All @@ -34,13 +35,13 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
<span className="py-2 pr-2 text-right font-semibold text-sm text-gray-500">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
{analytics.referenceData.length === 0 && (
{analytics.references.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.referenceData.map((reference) => (
{analytics.references.map((reference) => (
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm truncate text-gray-900 dark:text-gray-500">
{reference.name ? (
Expand Down Expand Up @@ -95,13 +96,13 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
{analytics.browserData.length === 0 && (
{analytics.browsers.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.browserData.map((reference) => (
{analytics.browsers.map((reference) => (
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate dark:text-gray-500">
{reference.name || "Unknown"}
Expand All @@ -118,13 +119,13 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200">
{analytics.deviceData.length === 0 && (
{analytics.devices.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.deviceData.map((device) => (
{analytics.devices.map((device) => (
<div key={device.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate">{device.name || "Unknown"}</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-gray-500 text-right shrink-0">{device.count}</span>
Expand Down
4 changes: 0 additions & 4 deletions frontend/web/src/helpers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@ export function signup(email: string, nickname: string, password: string) {
export function signout() {
return axios.post("/api/v1/auth/logout");
}

export function getShortcutAnalytics(shortcutId: number) {
return axios.get<AnalysisData>(`/api/v1/shortcut/${shortcutId}/analytics`);
}
20 changes: 0 additions & 20 deletions frontend/web/src/types/analytics.d.ts

This file was deleted.

13 changes: 0 additions & 13 deletions frontend/web/src/types/common.d.ts

This file was deleted.

21 changes: 21 additions & 0 deletions proto/api/v2/shortcut_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ service ShortcutService {
option (google.api.http) = {delete: "/api/v2/shortcuts/{id}"};
option (google.api.method_signature) = "id";
}
// GetShortcutAnalytics returns the analytics for a shortcut.
rpc GetShortcutAnalytics(GetShortcutAnalyticsRequest) returns (GetShortcutAnalyticsResponse) {
option (google.api.http) = {get: "/api/v2/shortcuts/{id}/analytics"};
option (google.api.method_signature) = "id";
}
}

message Shortcut {
Expand Down Expand Up @@ -115,3 +120,19 @@ message DeleteShortcutRequest {
}

message DeleteShortcutResponse {}

message GetShortcutAnalyticsRequest {
int32 id = 1;
}

message GetShortcutAnalyticsResponse {
message AnalyticsItem {
string name = 1;
int32 count = 2;
}
repeated AnalyticsItem references = 1;

repeated AnalyticsItem devices = 2;

repeated AnalyticsItem browsers = 3;
}
52 changes: 52 additions & 0 deletions proto/gen/api/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
- [CreateShortcutResponse](#slash-api-v2-CreateShortcutResponse)
- [DeleteShortcutRequest](#slash-api-v2-DeleteShortcutRequest)
- [DeleteShortcutResponse](#slash-api-v2-DeleteShortcutResponse)
- [GetShortcutAnalyticsRequest](#slash-api-v2-GetShortcutAnalyticsRequest)
- [GetShortcutAnalyticsResponse](#slash-api-v2-GetShortcutAnalyticsResponse)
- [GetShortcutAnalyticsResponse.AnalyticsItem](#slash-api-v2-GetShortcutAnalyticsResponse-AnalyticsItem)
- [GetShortcutRequest](#slash-api-v2-GetShortcutRequest)
- [GetShortcutResponse](#slash-api-v2-GetShortcutResponse)
- [ListShortcutsRequest](#slash-api-v2-ListShortcutsRequest)
Expand Down Expand Up @@ -434,6 +437,54 @@



<a name="slash-api-v2-GetShortcutAnalyticsRequest"></a>

### GetShortcutAnalyticsRequest



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |






<a name="slash-api-v2-GetShortcutAnalyticsResponse"></a>

### GetShortcutAnalyticsResponse



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| references | [GetShortcutAnalyticsResponse.AnalyticsItem](#slash-api-v2-GetShortcutAnalyticsResponse-AnalyticsItem) | repeated | |
| devices | [GetShortcutAnalyticsResponse.AnalyticsItem](#slash-api-v2-GetShortcutAnalyticsResponse-AnalyticsItem) | repeated | |
| browsers | [GetShortcutAnalyticsResponse.AnalyticsItem](#slash-api-v2-GetShortcutAnalyticsResponse-AnalyticsItem) | repeated | |






<a name="slash-api-v2-GetShortcutAnalyticsResponse-AnalyticsItem"></a>

### GetShortcutAnalyticsResponse.AnalyticsItem



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| name | [string](#string) | | |
| count | [int32](#int32) | | |






<a name="slash-api-v2-GetShortcutRequest"></a>

### GetShortcutRequest
Expand Down Expand Up @@ -582,6 +633,7 @@
| CreateShortcut | [CreateShortcutRequest](#slash-api-v2-CreateShortcutRequest) | [CreateShortcutResponse](#slash-api-v2-CreateShortcutResponse) | CreateShortcut creates a shortcut. |
| UpdateShortcut | [UpdateShortcutRequest](#slash-api-v2-UpdateShortcutRequest) | [UpdateShortcutResponse](#slash-api-v2-UpdateShortcutResponse) | UpdateShortcut updates a shortcut. |
| DeleteShortcut | [DeleteShortcutRequest](#slash-api-v2-DeleteShortcutRequest) | [DeleteShortcutResponse](#slash-api-v2-DeleteShortcutResponse) | DeleteShortcut deletes a shortcut by id. |
| GetShortcutAnalytics | [GetShortcutAnalyticsRequest](#slash-api-v2-GetShortcutAnalyticsRequest) | [GetShortcutAnalyticsResponse](#slash-api-v2-GetShortcutAnalyticsResponse) | GetShortcutAnalytics returns the analytics for a shortcut. |



Expand Down
Loading

0 comments on commit 3be52e7

Please sign in to comment.