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
2 changes: 2 additions & 0 deletions contract/contracts/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ impl AutoShareContract {
/// Emits a `BatchProcessingCompleted` event for off-chain listeners.
pub fn emit_batch_completed(env: Env, batch_id: BytesN<32>, processed_count: u32) {
autoshare_logic::emit_batch_completed(env, batch_id, processed_count).unwrap();
}

// ============================================================================
// Batch Notification Creation
// ============================================================================
Expand Down
10 changes: 0 additions & 10 deletions listener/package-lock.json

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

2 changes: 1 addition & 1 deletion listener/src/services/batch-validation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class BatchValidationService {
} else {
invalidEntries.push({
index,
error: validation.error,
error: validation.error ?? '',
});
}
});
Expand Down
64 changes: 64 additions & 0 deletions listener/src/services/notification-retry-queue.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { xdr } from '@stellar/stellar-sdk';
import * as StellarSDK from '@stellar/stellar-sdk';
import { NotificationRetryQueue, NotificationFn } from './notification-retry-queue';
import {
NotificationAnalyticsAggregator,
setNotificationAnalyticsAggregator,
} from './notification-analytics-aggregator';

jest.mock('../utils/logger', () => ({
__esModule: true,
Expand Down Expand Up @@ -296,4 +300,64 @@ describe('NotificationRetryQueue', () => {
queue.stop();
});
});

describe('analytics success counter (regression: no duplicate-counting)', () => {
let aggregator: NotificationAnalyticsAggregator;

beforeEach(() => {
aggregator = new NotificationAnalyticsAggregator();
setNotificationAnalyticsAggregator(aggregator);
});

afterEach(() => {
setNotificationAnalyticsAggregator(null);
});

it('increments success exactly once when an operation succeeds after multiple failed retries', async () => {
// Fails on attempts 1 and 2, succeeds on attempt 3.
let callCount = 0;
const notificationFn: NotificationFn = jest.fn().mockImplementation(async () => {
callCount++;
return callCount >= 3;
});

const queue = new NotificationRetryQueue(notificationFn, {
baseDelayMs: 100,
maxRetries: 5,
processIntervalMs: 50,
jitter: false,
});
queue.start();

queue.enqueue(createMockEvent({ id: 'evt-multi-retry' }), mockContractConfig, 'req-regression');

const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); };

// Attempt 1 at t=100 (base delay), fails → retry recorded
jest.advanceTimersByTime(100);
await flush();
// Attempt 2 at t=300 (100 + 100*2^1), fails → retry recorded
jest.advanceTimersByTime(200);
await flush();
// Attempt 3 at t=700 (300 + 100*2^2), succeeds → success recorded
jest.advanceTimersByTime(400);
await flush();

queue.stop();

const snap = aggregator.snapshot();

// The notification fn was called exactly 3 times
expect(notificationFn).toHaveBeenCalledTimes(3);

// Success must be exactly 1 — not 0 (missing) and not >1 (duplicate)
expect(snap.overall.success).toBe(1);

// Three retry-attempt events were emitted (one per call before success is known)
expect(snap.overall.retry).toBe(3);

// No failure outcome — the operation ultimately succeeded
expect(snap.overall.failure).toBe(0);
});
});
});
7 changes: 7 additions & 0 deletions listener/src/services/notification-retry-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ export class NotificationRetryQueue {

if (success) {
this.queuedFingerprints.delete(fingerprint);
this.analytics?.record({
notificationType: NotificationType.DISCORD,
contractAddress: item.contractConfig.address,
outcome: 'success',
durationMs: Date.now() - retryStart,
timestamp: Date.now(),
});
logger.info('Retry succeeded', {
requestId: item.requestId,
eventId: item.event.id,
Expand Down
Loading