Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ce3f362
feat(client): client call event reporting
jdimovska May 25, 2026
fcbc3de
feat(client): client call event reporting
jdimovska May 26, 2026
c469062
Merge branch 'refs/heads/main' into call-event-reporting
jdimovska May 26, 2026
75329b6
feat(client): client call event reporting
jdimovska May 26, 2026
e3e8e92
feat(client): client call event reporting
jdimovska May 26, 2026
4c03fc4
feat(client): client call event reporting
jdimovska May 26, 2026
b39e747
Merge branch 'refs/heads/main' into call-event-reporting
jdimovska May 26, 2026
76a8d64
feat(client): client call event reporting
jdimovska May 26, 2026
c524439
feat(client): client call event reporting
jdimovska May 27, 2026
9d1eb4b
feat(client): client call event reporting
jdimovska Jun 1, 2026
f8f2eb3
feat(client): client call event reporting
jdimovska Jun 1, 2026
b4c1518
feat(client): client call event reporting
jdimovska Jun 1, 2026
017fac9
feat(client): client call event reporting
jdimovska Jun 2, 2026
7acf2d6
feat(client): client call event reporting
jdimovska Jun 3, 2026
59373d5
feat(client): client call event reporting
jdimovska Jun 3, 2026
fba4393
feat(client): client call event reporting
jdimovska Jun 3, 2026
374495d
feat(client): client call event reporting
jdimovska Jun 3, 2026
cbd42f2
feat(client): client call event reporting
jdimovska Jun 3, 2026
782e224
feat(client): client call event reporting
jdimovska Jun 3, 2026
990f9ff
feat(client): client call event reporting
jdimovska Jun 3, 2026
61832da
feat(client): client call event reporting
jdimovska Jun 3, 2026
228f712
feat(client): client call event reporting
jdimovska Jun 4, 2026
bf76c10
feat(client): client call event reporting
jdimovska Jun 4, 2026
a891af5
feat(client): client call event reporting
jdimovska Jun 4, 2026
a5c3bea
feat(client): client call event reporting
jdimovska Jun 4, 2026
0e13479
Merge branch 'refs/heads/main' into call-event-reporting
jdimovska Jun 5, 2026
6ff2a8e
feat(client): client call event reporting
jdimovska Jun 5, 2026
5e9710d
feat(client): client call event reporting
jdimovska Jun 5, 2026
df28f52
feat(client): client call event reporting
jdimovska Jun 5, 2026
f43c46b
feat(client): client call event reporting
jdimovska Jun 5, 2026
78c282e
feat(client): client call event reporting
jdimovska Jun 5, 2026
876270d
feat(client): client call event reporting
jdimovska Jun 5, 2026
f832286
feat(client): client call event reporting
jdimovska Jun 6, 2026
95bf3a3
feat(client): client call event reporting
jdimovska Jun 6, 2026
5b5f7f8
feat(client): client call event reporting
jdimovska Jun 6, 2026
68f5121
feat(client): client call event reporting
jdimovska Jun 6, 2026
de439f7
feat(client): client call event reporting
jdimovska Jun 6, 2026
636151c
feat(client): client call event reporting
jdimovska Jun 8, 2026
565e66a
feat(client): client call event reporting
jdimovska Jun 8, 2026
dda5bf4
feat(client): client call event reporting
jdimovska Jun 8, 2026
e3a144d
feat(client): client call event reporting
jdimovska Jun 8, 2026
e9bddec
feat(client): client call event reporting
jdimovska Jun 8, 2026
fde4319
feat(client): client call event reporting
jdimovska Jun 8, 2026
cd3049b
feat(client): client call event reporting
jdimovska Jun 8, 2026
57d3045
feat(client): client call event reporting
jdimovska Jun 8, 2026
e0fcb0a
feat(client): client call event reporting
jdimovska Jun 8, 2026
b33a5b4
feat(client): client call event reporting
jdimovska Jun 8, 2026
2a0fab4
feat(client): client call event reporting
jdimovska Jun 8, 2026
11e682a
feat(client): client call event reporting
jdimovska Jun 8, 2026
47f31e0
feat(client): client call event reporting
jdimovska Jun 8, 2026
70fa296
feat(client): client call event reporting
jdimovska Jun 8, 2026
fca9742
feat(client): client call event reporting
jdimovska Jun 9, 2026
76441ab
feat(client): client call event reporting
jdimovska Jun 9, 2026
ca43807
feat(client): client call event reporting
jdimovska Jun 9, 2026
9e4383c
feat(client): client call event reporting
jdimovska Jun 9, 2026
e6b6f6a
feat(client): client call event reporting
jdimovska Jun 10, 2026
f9eb64b
feat(client): client call event reporting
jdimovska Jun 10, 2026
4e69b15
feat(client): client call event reporting
jdimovska Jun 10, 2026
d6ec58b
feat(client): client call event reporting
jdimovska Jun 10, 2026
0f7d21b
feat(client): client call event reporting
jdimovska Jun 10, 2026
d5bb94a
feat(client): client call event reporting
jdimovska Jun 10, 2026
5b3337e
feat(client): client call event reporting
jdimovska Jun 10, 2026
6851373
feat(client): client call event reporting
jdimovska Jun 11, 2026
8a982a7
feat(client): client call event reporting
jdimovska Jun 11, 2026
257ddb4
feat(client): client call event reporting
jdimovska Jun 11, 2026
cc2c118
feat(client): client call event reporting
jdimovska Jun 11, 2026
2ba0c8d
feat(client): client call event reporting
jdimovska Jun 11, 2026
2d65a9b
feat(client): client call event reporting
jdimovska Jun 11, 2026
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
244 changes: 184 additions & 60 deletions packages/client/src/Call.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/client/src/StreamSfuClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { withoutConcurrency } from './helpers/concurrency';
import { getTimers } from './timers';
import { Tracer, TraceSlice } from './stats';
import { SfuJoinError } from './errors';
import { SfuJoinError, SfuTimeoutError } from './errors';

export type StreamSfuClientConstructor = {
/**
Expand Down Expand Up @@ -353,7 +353,7 @@ export class StreamSfuClient {
timeoutId = setTimeout(() => {
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
this.tracer?.trace('signal.timeout', message);
reject(new Error(message));
reject(new SfuTimeoutError(message));
}, this.joinResponseTimeout);
}),
]),
Expand Down Expand Up @@ -644,7 +644,7 @@ export class StreamSfuClient {
cleanupJoinSubscriptions();
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
this.tracer?.trace('joinRequestTimeout', message);
current.reject(new Error(message));
current.reject(new SfuTimeoutError(message));
}, this.joinResponseTimeout);

const joinRequest = SfuRequest.create({
Expand Down
21 changes: 18 additions & 3 deletions packages/client/src/StreamVideoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { logToConsole, ScopedLogger, videoLoggerSystem } from './logger';
import { isReactNative } from './helpers/platforms';
import { withoutConcurrency } from './helpers/concurrency';
import { enableTimerWorker } from './timers';
import { ClientEventReporter } from './reporting';

/**
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
Expand All @@ -60,6 +61,7 @@ export class StreamVideoClient {

protected readonly writeableStateStore: StreamVideoWriteableStateStore;
streamClient: StreamClient;
readonly clientEventReporter: ClientEventReporter;

private effectsRegistered = false;
private eventHandlersToUnregister: Array<() => void> = [];
Expand Down Expand Up @@ -97,6 +99,9 @@ export class StreamVideoClient {
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;

this.streamClient = createCoordinatorClient(apiKey, clientOptions);
this.clientEventReporter = new ClientEventReporter({
streamClient: this.streamClient,
});

this.writeableStateStore = new StreamVideoWriteableStateStore();
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(
Expand Down Expand Up @@ -214,6 +219,7 @@ export class StreamVideoClient {

call = new Call({
streamClient: this.streamClient,
clientEventReporter: this.clientEventReporter,
type: e.call.type,
id: e.call.id,
members: e.members,
Expand Down Expand Up @@ -296,6 +302,9 @@ export class StreamVideoClient {
return this.connectAnonymousUser(user as UserWithId, tokenOrProvider);
}

const reporter = this.clientEventReporter;
reporter.startCoordinatorConnection(user.id);

const connectUserResponse = await withoutConcurrency(
this.connectionConcurrencyTag,
async () => {
Expand All @@ -309,13 +318,16 @@ export class StreamVideoClient {
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
try {
this.logger.trace(`Connecting user (${attempt})`, user);
return user.type === 'guest'
? await client.connectGuestUser(user)
: await client.connectUser(user, tokenOrProvider);
return await reporter.trackCoordinatorWs(() =>
user.type === 'guest'
? client.connectGuestUser(user)
: client.connectUser(user, tokenOrProvider),
);
} catch (err) {
this.logger.warn(`Failed to connect a user (${attempt})`, err);
errorQueue.push(err as Error);
if (attempt === maxConnectUserRetries - 1) {
reporter.closeCoordinatorWs();
onConnectUserError?.(err as Error, errorQueue);
throw err;
}
Expand Down Expand Up @@ -415,6 +427,7 @@ export class StreamVideoClient {
call ??
new Call({
streamClient: this.streamClient,
clientEventReporter: this.clientEventReporter,
id: id,
type: type,
clientStore: this.writeableStateStore,
Expand Down Expand Up @@ -448,6 +461,7 @@ export class StreamVideoClient {
for (const c of response.calls) {
const call = new Call({
streamClient: this.streamClient,
clientEventReporter: this.clientEventReporter,
id: c.call.id,
type: c.call.type,
members: c.members,
Expand Down Expand Up @@ -591,6 +605,7 @@ export class StreamVideoClient {
const [callType, callId] = call_cid.split(':');
call = new Call({
streamClient: this.streamClient,
clientEventReporter: this.clientEventReporter,
type: callType,
id: callId,
clientStore: this.writeableStateStore,
Expand Down
5 changes: 4 additions & 1 deletion packages/client/src/__tests__/Call.autodrop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '../rtc/__tests__/mocks/webrtc.mocks';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Call } from '../Call';
import { StreamClient } from '../coordinator/connection/client';
import { ClientEventReporter } from '../reporting';
import { generateUUIDv4 } from '../coordinator/connection/utils';
import { CallingState, StreamVideoWriteableStateStore } from '../store';

Expand All @@ -14,10 +15,12 @@ describe('Auto drop ringing calls', () => {
vi.useFakeTimers();

const clientStore = new StreamVideoWriteableStateStore();
const streamClient = new StreamClient('abc');
call = new Call({
type: 'test',
id: generateUUIDv4(),
streamClient: new StreamClient('abc'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: clientStore,
});

Expand Down
5 changes: 4 additions & 1 deletion packages/client/src/__tests__/Call.lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import '../rtc/__tests__/mocks/webrtc.mocks';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Call } from '../Call';
import { StreamClient } from '../coordinator/connection/client';
import { ClientEventReporter } from '../reporting';
import { generateUUIDv4 } from '../coordinator/connection/utils';
import { StreamVideoWriteableStateStore } from '../store';

describe('Call lifecycle wiring', () => {
let call: Call;

beforeEach(() => {
const streamClient = new StreamClient('abc');
call = new Call({
type: 'test',
id: generateUUIDv4(),
streamClient: new StreamClient('abc'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
});
});
Expand Down
5 changes: 4 additions & 1 deletion packages/client/src/__tests__/Call.publishing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Call } from '../Call';
import { Publisher } from '../rtc';
import { StreamClient } from '../coordinator/connection/client';
import { ClientEventReporter } from '../reporting';
import { generateUUIDv4 } from '../coordinator/connection/utils';
import { PermissionsContext } from '../permissions';
import { OwnCapability } from '../gen/coordinator';
Expand All @@ -15,10 +16,12 @@ describe('Publishing and Unpublishing tracks', () => {
let call: Call;

beforeEach(async () => {
const streamClient = new StreamClient('abc');
call = new Call({
type: 'test',
id: generateUUIDv4(),
streamClient: new StreamClient('abc'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
});

Expand Down
23 changes: 23 additions & 0 deletions packages/client/src/__tests__/Call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,29 @@ describe('muting logic', () => {
});
});

describe('client event reporting', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('reports a client-aborted event and unregisters the call on leave', async () => {
const call = client.call('default', generateUUIDv4());
await call.getOrCreate();

const reporter = call.clientEventReporter;
const abortSpy = vi.spyOn(reporter, 'abort');
const unregisterSpy = vi.spyOn(reporter, 'unregisterCall');

await call.leave();

expect(abortSpy).toHaveBeenCalledWith(call.cid, {
code: 'CLIENT_ABORTED',
reason: expect.any(String),
});
expect(unregisterSpy).toHaveBeenCalledWith(call.cid);
});
});

afterEach(() => {
client.disconnectUser();
});
5 changes: 5 additions & 0 deletions packages/client/src/coordinator/connection/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,11 @@ export class StreamClient {
return await this.wsConnection.connect(this.defaultWSTimeout);
};

getSdkVersion = (): string =>
this.options.clientAppIdentifier?.sdkVersion ||
process.env.PKG_VERSION ||
'0.0.0';

getUserAgent = (): string => {
if (!this.cachedUserAgent) {
const { clientAppIdentifier = {} } = this.options;
Expand Down
11 changes: 10 additions & 1 deletion packages/client/src/devices/__tests__/CameraManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { ClientEventReporter } from '../../reporting';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';

import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
Expand Down Expand Up @@ -56,6 +57,12 @@ vi.mock('../../Call.ts', () => {
};
});

vi.mock('../../reporting/ClientEventReporter', () => ({
ClientEventReporter: vi.fn(function () {
return {};
}),
}));

vi.mock('../../helpers/compatibility.ts', () => {
console.log('MOCKING mobile device');
return {
Expand All @@ -76,10 +83,12 @@ describe('CameraManager', () => {

beforeEach(() => {
const devicePersistence = { enabled: false, storageKey: '' };
const streamClient = new StreamClient('abc123', { devicePersistence });
call = new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123', { devicePersistence }),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
});
manager = new CameraManager(call, devicePersistence);
Expand Down
11 changes: 10 additions & 1 deletion packages/client/src/devices/__tests__/DeviceManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* @vitest-environment happy-dom */
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { ClientEventReporter } from '../../reporting';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
Expand All @@ -22,6 +23,12 @@ import { TrackType } from '../../gen/video/sfu/models/models';
import { PermissionsContext } from '../../permissions';
import { readPreferences } from '../devicePersistence';

vi.mock('../../reporting/ClientEventReporter', () => ({
ClientEventReporter: vi.fn(function () {
return {};
}),
}));

vi.mock('../../Call.ts', () => {
console.log('MOCKING Call');
return {
Expand Down Expand Up @@ -85,11 +92,13 @@ describe('Device Manager', () => {
configurable: true,
value: localStorageMock,
});
const streamClient = new StreamClient('abc123');
manager = new TestInputMediaDeviceManager(
new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
}),
{ enabled: false, storageKey },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { of } from 'rxjs';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { ClientEventReporter } from '../../reporting';
import { StreamVideoWriteableStateStore } from '../../store';
import { DeviceManagerState } from '../DeviceManagerState';
import { DeviceManager } from '../DeviceManager';
Expand Down Expand Up @@ -45,11 +46,13 @@ describe('MediaStream Filters', () => {
let manager: TestInputMediaDeviceManager;

beforeEach(() => {
const streamClient = new StreamClient('abc123');
manager = new TestInputMediaDeviceManager(
new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
}),
);
Expand Down
11 changes: 10 additions & 1 deletion packages/client/src/devices/__tests__/MicrophoneManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
readPreferences,
toPreferenceList,
} from '../devicePersistence';
import { ClientEventReporter } from '../../reporting';

vi.mock('../devices.ts', () => {
console.log('MOCKING devices API');
Expand Down Expand Up @@ -82,6 +83,12 @@ vi.mock('../../Call.ts', () => {
};
});

vi.mock('../../reporting/ClientEventReporter', () => ({
ClientEventReporter: vi.fn(function () {
return {};
}),
}));

describe('MicrophoneManager', () => {
let manager: MicrophoneManager;
let call: Call;
Expand All @@ -92,10 +99,12 @@ describe('MicrophoneManager', () => {
of('granted'),
);

const streamClient = new StreamClient('abc123');
call = new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
});
const devicePersistence = { enabled: false, storageKey: '' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { of } from 'rxjs';
import '../../rtc/__tests__/mocks/webrtc.mocks';
import { OwnCapability } from '../../gen/coordinator';
import { settled, withoutConcurrency } from '../../helpers/concurrency';
import { ClientEventReporter } from '../../reporting';

let speechActivityCallback:
| ((state: { isSoundDetected: boolean }) => void)
Expand Down Expand Up @@ -82,12 +83,14 @@ describe('MicrophoneManager React Native', () => {
};

const devicePersistence = { enabled: false, storageKey: '' };
const streamClient = new StreamClient('abc123');
manager = new MicrophoneManager(
new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
streamClient,
clientStore: new StreamVideoWriteableStateStore(),
clientEventReporter: new ClientEventReporter({ streamClient }),
}),
devicePersistence,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ScreenShareManager } from '../ScreenShareManager';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { ClientEventReporter } from '../../reporting';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
import * as RxUtils from '../../store/rxUtils';
import { mockCall, mockDeviceIds$, mockScreenShareStream } from './mocks';
Expand Down Expand Up @@ -36,11 +37,13 @@ describe('ScreenShareManager', () => {
let manager: ScreenShareManager;

beforeEach(() => {
const streamClient = new StreamClient('abc123');
manager = new ScreenShareManager(
new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
streamClient,
clientEventReporter: new ClientEventReporter({ streamClient }),
clientStore: new StreamVideoWriteableStateStore(),
}),
);
Expand Down
Loading
Loading