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
26 changes: 26 additions & 0 deletions backend/internal/data/shutter_explorer.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions backend/internal/data/sql/queries/shutter_explorer.sql
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ LIMIT $1;
SELECT COUNT(id), tx_status FROM decrypted_tx
GROUP BY tx_status;

-- name: QueryExecutedTransactionStatsRecent :many

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The chart currently windows by calendar time but we want to show the inclusion rate over the latest X finalized transactions. The goal being to surface that the recent runs succeeded, regardless of when they ran.

Since we don't have a test continuously sending transactions, a time-based window can end up with zero rows in the selected range and render an empty/NaN gauge, even when the latest runs were all successful, which is the case we most want to show.

Could we switch to a count-based window instead? Showing the latest X transactions, where X would be 100, 1000, ALL for example. See my comment here: https://github.com/shutter-network/shutter-technical-pm/issues/132#issuecomment-4768221376

SELECT COUNT(id), tx_status FROM decrypted_tx
WHERE created_at >= NOW() - ($1::int * INTERVAL '1 day')
GROUP BY tx_status;

-- name: QueryHistoricalInclusionTimes :many
WITH daily_inclusion_times AS (
SELECT
Expand Down
18 changes: 18 additions & 0 deletions backend/internal/service/inclusion_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package service

import (
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/shutter-network/shutter-explorer/backend/internal/usecase"
Expand Down Expand Up @@ -39,6 +40,23 @@ func (svc *InclusionTimeService) QueryExecutedTransactionStats(ctx *gin.Context)
})
}

func (svc *InclusionTimeService) QueryExecutedTransactionStatsRecent(ctx *gin.Context) {
daysStr := ctx.DefaultQuery("days", "30")
days, err := strconv.Atoi(daysStr)
if err != nil || days <= 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid days parameter"})
return
}
stats, httpErr := svc.InclusionTimeUsecase.QueryExecutedTransactionStatsRecent(ctx, days)
if httpErr != nil {
ctx.Error(httpErr)
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": stats,
})
}

func (svc *InclusionTimeService) QueryHistoricalInclusionTimes(ctx *gin.Context) {
historicalInclusionTimes, err := svc.InclusionTimeUsecase.QueryHistoricalInclusionTimes(ctx)
if err != nil {
Expand Down
34 changes: 34 additions & 0 deletions backend/internal/usecase/inclusion_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,40 @@ func (uc *InclusionTimeUsecase) QueryExecutedTransactionStats(ctx context.Contex
return resp, nil
}

func (uc *InclusionTimeUsecase) QueryExecutedTransactionStatsRecent(ctx context.Context, days int) (*QueryExectuedTransactionStatsResp, *error.Http) {
stats, err := uc.observerDBQuery.QueryExecutedTransactionStatsRecent(ctx, int32(days))
if err != nil {
log.Err(err).Msg("err encountered while querying DB")
err := error.NewHttpError(
"error encountered while querying for data",
"",
http.StatusInternalServerError,
)
return nil, &err
}

resp := &QueryExectuedTransactionStatsResp{}
for i := 0; i < len(stats); i++ {
resp.Total += stats[i].Count
switch stats[i].TxStatus {
case data.TxStatusValInvalid:
resp.Invalid = stats[i].Count
case data.TxStatusValNotdecrypted:
resp.NotDecrypted = stats[i].Count
case data.TxStatusValNotincluded:
resp.NotIncluded = stats[i].Count
case data.TxStatusValPending:
resp.Pending = stats[i].Count
case data.TxStatusValShieldedinclusion:
resp.Shielded = stats[i].Count
case data.TxStatusValUnshieldedinclusion:
resp.Unshielded = stats[i].Count
}
}

return resp, nil
}

func (uc *InclusionTimeUsecase) QueryHistoricalInclusionTimes(ctx context.Context) ([]data.QueryHistoricalInclusionTimesRow, *error.Http) {
historicalInclusionTimes, err := uc.observerDBQuery.QueryHistoricalInclusionTimes(ctx)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions backend/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func NewRouter(ctx context.Context, usecases *usecase.Usecases) *gin.Engine {
inclusionTime := api.Group("inclusion_time")
inclusionTime.GET("/estimated_inclusion_time", inclusionTimeService.QueryEstimatedInclusionTime)
inclusionTime.GET("/executed_transactions", inclusionTimeService.QueryExecutedTransactionStats)
inclusionTime.GET("/executed_transactions_recent", inclusionTimeService.QueryExecutedTransactionStatsRecent)
inclusionTime.GET("/historical_inclusion_time", inclusionTimeService.QueryHistoricalInclusionTimes)
}
return router
Expand Down
75 changes: 49 additions & 26 deletions frontend/cypress/components/TransactionGauge.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
import { customTheme, muiTheme } from '../../src/theme';

const mountGauge = () => {
const mockSocket = {
onopen: cy.stub(),
onmessage: cy.stub(),
onclose: cy.stub(),
onerror: cy.stub(),
};
return mount(
<MUIThemeProvider theme={muiTheme}>
<StyledThemeProvider theme={customTheme}>
<WebSocketContext.Provider value={{ socket: mockSocket as unknown as WebSocket }}>
<MemoryRouter>
<TransactionGauge />
</MemoryRouter>
</WebSocketContext.Provider>
</StyledThemeProvider>
</MUIThemeProvider>
);
};

describe('<TransactionGauge />', () => {
it('renders the transaction gauge with test values', () => {
const mockSocket = {
onopen: cy.stub(),
onmessage: cy.stub(),
onclose: cy.stub(),
onerror: cy.stub(),
};

cy.intercept('GET', '/api/inclusion_time/executed_transactions', {
it('renders the gauge with default 30-day window', () => {
cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=30', {
statusCode: 200,
body: {
message: {
Expand All @@ -25,32 +38,42 @@ describe('<TransactionGauge />', () => {
Total: 30
}
},
}).as('getExecutedTransactions');

mount(
<MUIThemeProvider theme={muiTheme}>
<StyledThemeProvider theme={customTheme}>
<WebSocketContext.Provider value={{ socket: mockSocket as unknown as WebSocket }}>
<MemoryRouter>
<TransactionGauge />
</MemoryRouter>
</WebSocketContext.Provider>
</StyledThemeProvider>
</MUIThemeProvider>
);

cy.wait('@getExecutedTransactions');
}).as('getRecentTransactions');

mountGauge();

cy.wait('@getRecentTransactions');

cy.get('div[role="meter"]').invoke('css', 'height', '300px');
cy.get('div[role="meter"]').should('exist').and('be.visible');

cy.contains('Last 30 days').should('be.visible');
cy.contains('Shielded').should('be.visible');
cy.contains('25').should('be.visible');

cy.contains('Total').should('be.visible');
cy.contains('30').should('be.visible');

cy.contains('Unshielded').should('be.visible');
cy.contains('5').should('be.visible');
});

it('switches to 7-day window when 7d button is clicked', () => {
cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=30', {
statusCode: 200,
body: { message: { Shielded: 25, Unshielded: 5 } },
});

cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=7', {
statusCode: 200,
body: { message: { Shielded: 10, Unshielded: 2 } },
}).as('get7DayTransactions');

mountGauge();

cy.contains('7d').click();
cy.wait('@get7DayTransactions');

cy.contains('Last 7 days').should('be.visible');
cy.contains('10').should('be.visible');
cy.contains('2').should('be.visible');
});
});
90 changes: 40 additions & 50 deletions frontend/src/modules/TransactionGauge.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,59 @@
import { Alert, Box } from '@mui/material';
import { Alert, Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
import OverviewCard from '../components/OverviewCard';
import BasicGauges from '../components/Gauge';
import { useEffect, useState } from 'react';
import { useWebSocket } from '../context/WebSocketContext';
import { useState } from 'react';
import useFetch from '../hooks/useFetch';

const TransactionGauge = () => {
const { data: transactionStatsData, loading: loadingTransactionStats, error: errorTransactionStats } = useFetch('/api/inclusion_time/executed_transactions');
const [successfulTransactions, setSuccessfulTransactions] = useState<number>(transactionStatsData?.message?.Shielded || 0);
const [failedTransactions, setFailedTransactions] = useState<number>(transactionStatsData?.message?.Unshielded || 0);
const [, setWebSocketError] = useState<string | null>(null);

const { socket } = useWebSocket()!;

useEffect(() => {
if (socket) {
socket.onmessage = (event: MessageEvent) => {
const websocketEvent = JSON.parse(event.data);
if (websocketEvent.error) {
setWebSocketError(`Error: ${websocketEvent.error.message} (Code: ${websocketEvent.error.code})`);
} else if (websocketEvent.data) {
setWebSocketError(null);
if (websocketEvent.type === 'executed_transactions_updated') {
if ('Shielded' in websocketEvent.data.message && 'Unshielded' in websocketEvent.data.message) {
setSuccessfulTransactions(websocketEvent.data.message.Shielded);
setFailedTransactions(websocketEvent.data.message.Unshielded);
}
}
}
};
const WINDOW_OPTIONS = [7, 30, 90, 'all'] as const;
type WindowOption = typeof WINDOW_OPTIONS[number];

socket.onerror = () => {
setWebSocketError('WebSocket error: A connection error occurred');
};
}

return () => {
if (socket) {
socket.onmessage = null;
socket.onerror = null;
}
};
}, [socket]);
const TransactionGauge = () => {
const [window, setWindow] = useState<WindowOption>(30);
const url = window === 'all'
? '/api/inclusion_time/executed_transactions'
: `/api/inclusion_time/executed_transactions_recent?days=${window}`;
const { data, loading, error } = useFetch(url);

useEffect(() => {
if (transactionStatsData?.message?.Shielded) setSuccessfulTransactions(transactionStatsData.message.Shielded);
if (transactionStatsData?.message?.Unshielded) setFailedTransactions(transactionStatsData.message.Unshielded);
}, [transactionStatsData]);
const shielded: number = data?.message?.Shielded || 0;
const unshielded: number = data?.message?.Unshielded || 0;
const total = shielded + unshielded;

const totalTransactions = successfulTransactions + failedTransactions
return (
<Box sx={{ flexGrow: 1, marginTop: 4 }}>
<OverviewCard title="Shielded Transactions" centerTitle>
{errorTransactionStats ? (
<Alert severity="error">Error fetching Transaction Stats: {errorTransactionStats.message}</Alert>
<Box display="flex" justifyContent="center" mb={1}>
<ToggleButtonGroup
value={window}
exclusive
onChange={(_, v) => { if (v !== null) setWindow(v as WindowOption); }}
size="small"
>
{WINDOW_OPTIONS.map((d) => (
<ToggleButton key={d} value={d}>
{d === 'all' ? 'All' : `${d}d`}
</ToggleButton>
))}
</ToggleButtonGroup>
</Box>
<Typography
variant="body2"
align="center"
sx={{ color: 'text.secondary', mb: 1 }}
>
{window === 'all' ? 'All time' : `Last ${window} days`}
</Typography>
{error ? (
<Alert severity="error">Error fetching Transaction Stats: {error.message}</Alert>
) : (
<BasicGauges
success={loadingTransactionStats ? 0 : successfulTransactions}
total={loadingTransactionStats ? 0 : totalTransactions}
failed={loadingTransactionStats ? 0 : failedTransactions}
success={loading ? 0 : shielded}
total={loading ? 0 : total}
failed={loading ? 0 : unshielded}
/>
)}
</OverviewCard>
</Box>
);
};

export default TransactionGauge;
export default TransactionGauge;
Loading