@vapi-ai/web 2.3.7 → 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vapi.d.ts CHANGED
@@ -19,7 +19,27 @@ export interface SayMessage {
19
19
  interruptAssistantEnabled?: boolean;
20
20
  }
21
21
  type VapiClientToServerMessage = AddMessageMessage | ControlMessages | SayMessage;
22
- type VapiEventNames = 'call-end' | 'call-start' | 'volume-level' | 'speech-start' | 'speech-end' | 'message' | 'video' | 'error' | 'daily-participant-updated';
22
+ type VapiEventNames = 'call-end' | 'call-start' | 'volume-level' | 'speech-start' | 'speech-end' | 'message' | 'video' | 'error' | 'camera-error' | 'network-quality-change' | 'network-connection' | 'daily-participant-updated' | 'call-start-progress' | 'call-start-success' | 'call-start-failed';
23
+ interface CallStartProgressEvent {
24
+ stage: string;
25
+ status: 'started' | 'completed' | 'failed';
26
+ duration?: number;
27
+ timestamp: string;
28
+ metadata?: Record<string, any>;
29
+ }
30
+ interface CallStartSuccessEvent {
31
+ totalDuration: number;
32
+ callId?: string;
33
+ timestamp: string;
34
+ }
35
+ interface CallStartFailedEvent {
36
+ stage: string;
37
+ totalDuration: number;
38
+ error: string;
39
+ errorStack?: string;
40
+ timestamp: string;
41
+ context: Record<string, any>;
42
+ }
23
43
  type VapiEventListeners = {
24
44
  'call-end': () => void;
25
45
  'call-start': () => void;
@@ -29,7 +49,13 @@ type VapiEventListeners = {
29
49
  video: (track: MediaStreamTrack) => void;
30
50
  message: (message: any) => void;
31
51
  error: (error: any) => void;
52
+ 'camera-error': (error: any) => void;
53
+ 'network-quality-change': (event: any) => void;
54
+ 'network-connection': (event: any) => void;
32
55
  'daily-participant-updated': (participant: DailyParticipant) => void;
56
+ 'call-start-progress': (event: CallStartProgressEvent) => void;
57
+ 'call-start-success': (event: CallStartSuccessEvent) => void;
58
+ 'call-start-failed': (event: CallStartFailedEvent) => void;
33
59
  };
34
60
  declare class VapiEventEmitter extends EventEmitter {
35
61
  on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
package/dist/vapi.js CHANGED
@@ -88,14 +88,46 @@ class Vapi extends VapiEventEmitter {
88
88
  return new Promise((resolve) => setTimeout(resolve, ms));
89
89
  }
90
90
  async start(assistant, assistantOverrides, squad, workflow, workflowOverrides) {
91
+ const startTime = Date.now();
92
+ // Input validation with detailed error messages
91
93
  if (!assistant && !squad && !workflow) {
92
- throw new Error('Assistant or Squad or Workflow must be provided.');
94
+ const error = new Error('Assistant or Squad or Workflow must be provided.');
95
+ this.emit('error', {
96
+ type: 'validation-error',
97
+ stage: 'input-validation',
98
+ message: error.message,
99
+ timestamp: new Date().toISOString()
100
+ });
101
+ throw error;
93
102
  }
94
103
  if (this.started) {
104
+ this.emit('call-start-progress', {
105
+ stage: 'validation',
106
+ status: 'failed',
107
+ timestamp: new Date().toISOString(),
108
+ metadata: { reason: 'already-started' }
109
+ });
95
110
  return null;
96
111
  }
112
+ this.emit('call-start-progress', {
113
+ stage: 'initialization',
114
+ status: 'started',
115
+ timestamp: new Date().toISOString(),
116
+ metadata: {
117
+ hasAssistant: !!assistant,
118
+ hasSquad: !!squad,
119
+ hasWorkflow: !!workflow
120
+ }
121
+ });
97
122
  this.started = true;
98
123
  try {
124
+ // Stage 1: Create web call
125
+ this.emit('call-start-progress', {
126
+ stage: 'web-call-creation',
127
+ status: 'started',
128
+ timestamp: new Date().toISOString()
129
+ });
130
+ const webCallStartTime = Date.now();
99
131
  const webCall = (await client_1.client.call.callControllerCreateWebCall({
100
132
  assistant: typeof assistant === 'string' ? undefined : assistant,
101
133
  assistantId: typeof assistant === 'string' ? assistant : undefined,
@@ -106,16 +138,73 @@ class Vapi extends VapiEventEmitter {
106
138
  workflowId: typeof workflow === 'string' ? workflow : undefined,
107
139
  workflowOverrides,
108
140
  })).data;
141
+ const webCallDuration = Date.now() - webCallStartTime;
142
+ this.emit('call-start-progress', {
143
+ stage: 'web-call-creation',
144
+ status: 'completed',
145
+ duration: webCallDuration,
146
+ timestamp: new Date().toISOString(),
147
+ metadata: {
148
+ callId: webCall?.id || 'unknown',
149
+ videoRecordingEnabled: webCall?.artifactPlan?.videoRecordingEnabled ?? false,
150
+ voiceProvider: webCall?.assistant?.voice?.provider || 'unknown'
151
+ }
152
+ });
109
153
  if (this.call) {
154
+ this.emit('call-start-progress', {
155
+ stage: 'daily-call-object-creation',
156
+ status: 'started',
157
+ timestamp: new Date().toISOString(),
158
+ metadata: { action: 'cleanup-existing' }
159
+ });
110
160
  this.cleanup();
111
161
  }
112
162
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
113
163
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
114
- this.call = daily_js_1.default.createCallObject({
115
- audioSource: this.dailyCallObject.audioSource ?? true,
116
- videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
117
- dailyConfig: this.dailyCallConfig,
164
+ // Stage 2: Create Daily call object
165
+ this.emit('call-start-progress', {
166
+ stage: 'daily-call-object-creation',
167
+ status: 'started',
168
+ timestamp: new Date().toISOString(),
169
+ metadata: {
170
+ audioSource: this.dailyCallObject.audioSource ?? true,
171
+ videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
172
+ isVideoRecordingEnabled,
173
+ isVideoEnabled
174
+ }
118
175
  });
176
+ const dailyCallStartTime = Date.now();
177
+ try {
178
+ this.call = daily_js_1.default.createCallObject({
179
+ audioSource: this.dailyCallObject.audioSource ?? true,
180
+ videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
181
+ dailyConfig: this.dailyCallConfig,
182
+ });
183
+ const dailyCallDuration = Date.now() - dailyCallStartTime;
184
+ this.emit('call-start-progress', {
185
+ stage: 'daily-call-object-creation',
186
+ status: 'completed',
187
+ duration: dailyCallDuration,
188
+ timestamp: new Date().toISOString()
189
+ });
190
+ }
191
+ catch (error) {
192
+ const dailyCallDuration = Date.now() - dailyCallStartTime;
193
+ this.emit('call-start-progress', {
194
+ stage: 'daily-call-object-creation',
195
+ status: 'failed',
196
+ duration: dailyCallDuration,
197
+ timestamp: new Date().toISOString(),
198
+ metadata: { error: error?.toString() }
199
+ });
200
+ this.emit('error', {
201
+ type: 'daily-call-object-creation-error',
202
+ stage: 'daily-call-object-creation',
203
+ error,
204
+ timestamp: new Date().toISOString()
205
+ });
206
+ throw error;
207
+ }
119
208
  this.call.iframe()?.style.setProperty('display', 'none');
120
209
  this.call.on('left-meeting', () => {
121
210
  this.emit('call-end');
@@ -139,7 +228,13 @@ class Vapi extends VapiEventEmitter {
139
228
  }
140
229
  });
141
230
  this.call.on('camera-error', (error) => {
142
- this.emit('error', error);
231
+ this.emit('camera-error', error);
232
+ });
233
+ this.call.on('network-quality-change', (event) => {
234
+ this.emit('network-quality-change', event);
235
+ });
236
+ this.call.on('network-connection', (event) => {
237
+ this.emit('network-connection', event);
143
238
  });
144
239
  this.call.on('track-started', async (e) => {
145
240
  if (!e || !e.participant) {
@@ -176,35 +271,173 @@ class Vapi extends VapiEventEmitter {
176
271
  }
177
272
  destroyAudioPlayer(e.participant.session_id);
178
273
  });
179
- // Allow mobile devices to finish processing the microphone permissions
180
- // request before joining the call and playing the assistant's audio
181
- if (this.isMobileDevice()) {
274
+ // Stage 3: Mobile device handling and permissions
275
+ const isMobile = this.isMobileDevice();
276
+ this.emit('call-start-progress', {
277
+ stage: 'mobile-permissions',
278
+ status: 'started',
279
+ timestamp: new Date().toISOString(),
280
+ metadata: { isMobile }
281
+ });
282
+ if (isMobile) {
283
+ const mobileWaitStartTime = Date.now();
182
284
  await this.sleep(1000);
285
+ const mobileWaitDuration = Date.now() - mobileWaitStartTime;
286
+ this.emit('call-start-progress', {
287
+ stage: 'mobile-permissions',
288
+ status: 'completed',
289
+ duration: mobileWaitDuration,
290
+ timestamp: new Date().toISOString(),
291
+ metadata: { action: 'permissions-wait' }
292
+ });
183
293
  }
184
- await this.call.join({
185
- // @ts-expect-error This exists
186
- url: webCall.webCallUrl,
187
- subscribeToTracksAutomatically: false,
294
+ else {
295
+ this.emit('call-start-progress', {
296
+ stage: 'mobile-permissions',
297
+ status: 'completed',
298
+ timestamp: new Date().toISOString(),
299
+ metadata: { action: 'skipped-not-mobile' }
300
+ });
301
+ }
302
+ // Stage 4: Join the call
303
+ this.emit('call-start-progress', {
304
+ stage: 'daily-call-join',
305
+ status: 'started',
306
+ timestamp: new Date().toISOString()
188
307
  });
308
+ const joinStartTime = Date.now();
309
+ try {
310
+ await this.call.join({
311
+ // @ts-expect-error This exists
312
+ url: webCall.webCallUrl,
313
+ subscribeToTracksAutomatically: false,
314
+ });
315
+ const joinDuration = Date.now() - joinStartTime;
316
+ this.emit('call-start-progress', {
317
+ stage: 'daily-call-join',
318
+ status: 'completed',
319
+ duration: joinDuration,
320
+ timestamp: new Date().toISOString()
321
+ });
322
+ }
323
+ catch (error) {
324
+ const joinDuration = Date.now() - joinStartTime;
325
+ this.emit('call-start-progress', {
326
+ stage: 'daily-call-join',
327
+ status: 'failed',
328
+ duration: joinDuration,
329
+ timestamp: new Date().toISOString(),
330
+ metadata: { error: error?.toString() }
331
+ });
332
+ this.emit('error', {
333
+ type: 'daily-call-join-error',
334
+ stage: 'daily-call-join',
335
+ error,
336
+ duration: joinDuration,
337
+ timestamp: new Date().toISOString()
338
+ });
339
+ throw error;
340
+ }
341
+ // Stage 5: Video recording setup (if enabled)
189
342
  if (isVideoRecordingEnabled) {
190
- const recordingRequestedTime = new Date().getTime();
191
- this.call.startRecording({
192
- width: 1280,
193
- height: 720,
194
- backgroundColor: '#FF1F2D3D',
195
- layout: {
196
- preset: 'default',
197
- },
343
+ this.emit('call-start-progress', {
344
+ stage: 'video-recording-setup',
345
+ status: 'started',
346
+ timestamp: new Date().toISOString()
198
347
  });
199
- this.call.on('recording-started', () => {
200
- this.send({
201
- type: 'control',
202
- control: 'say-first-message',
203
- videoRecordingStartDelaySeconds: (new Date().getTime() - recordingRequestedTime) / 1000,
348
+ const recordingRequestedTime = new Date().getTime();
349
+ const recordingStartTime = Date.now();
350
+ try {
351
+ this.call.startRecording({
352
+ width: 1280,
353
+ height: 720,
354
+ backgroundColor: '#FF1F2D3D',
355
+ layout: {
356
+ preset: 'default',
357
+ },
358
+ });
359
+ const recordingSetupDuration = Date.now() - recordingStartTime;
360
+ this.emit('call-start-progress', {
361
+ stage: 'video-recording-setup',
362
+ status: 'completed',
363
+ duration: recordingSetupDuration,
364
+ timestamp: new Date().toISOString()
365
+ });
366
+ this.call.on('recording-started', () => {
367
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
368
+ this.emit('call-start-progress', {
369
+ stage: 'video-recording-started',
370
+ status: 'completed',
371
+ timestamp: new Date().toISOString(),
372
+ metadata: { delaySeconds: totalRecordingDelay }
373
+ });
374
+ this.send({
375
+ type: 'control',
376
+ control: 'say-first-message',
377
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
378
+ });
379
+ });
380
+ }
381
+ catch (error) {
382
+ const recordingSetupDuration = Date.now() - recordingStartTime;
383
+ this.emit('call-start-progress', {
384
+ stage: 'video-recording-setup',
385
+ status: 'failed',
386
+ duration: recordingSetupDuration,
387
+ timestamp: new Date().toISOString(),
388
+ metadata: { error: error?.toString() }
389
+ });
390
+ this.emit('error', {
391
+ type: 'video-recording-setup-error',
392
+ stage: 'video-recording-setup',
393
+ error,
394
+ timestamp: new Date().toISOString()
204
395
  });
396
+ // Don't throw here, video recording is optional
397
+ }
398
+ }
399
+ else {
400
+ this.emit('call-start-progress', {
401
+ stage: 'video-recording-setup',
402
+ status: 'completed',
403
+ timestamp: new Date().toISOString(),
404
+ metadata: { action: 'skipped-not-enabled' }
405
+ });
406
+ }
407
+ // Stage 6: Audio level observer setup
408
+ this.emit('call-start-progress', {
409
+ stage: 'audio-observer-setup',
410
+ status: 'started',
411
+ timestamp: new Date().toISOString()
412
+ });
413
+ const audioObserverStartTime = Date.now();
414
+ try {
415
+ this.call.startRemoteParticipantsAudioLevelObserver(100);
416
+ const audioObserverDuration = Date.now() - audioObserverStartTime;
417
+ this.emit('call-start-progress', {
418
+ stage: 'audio-observer-setup',
419
+ status: 'completed',
420
+ duration: audioObserverDuration,
421
+ timestamp: new Date().toISOString()
422
+ });
423
+ }
424
+ catch (error) {
425
+ const audioObserverDuration = Date.now() - audioObserverStartTime;
426
+ this.emit('call-start-progress', {
427
+ stage: 'audio-observer-setup',
428
+ status: 'failed',
429
+ duration: audioObserverDuration,
430
+ timestamp: new Date().toISOString(),
431
+ metadata: { error: error?.toString() }
432
+ });
433
+ this.emit('error', {
434
+ type: 'audio-observer-setup-error',
435
+ stage: 'audio-observer-setup',
436
+ error,
437
+ timestamp: new Date().toISOString()
205
438
  });
439
+ // Don't throw here, this is non-critical
206
440
  }
207
- this.call.startRemoteParticipantsAudioLevelObserver(100);
208
441
  this.call.on('remote-participants-audio-level', (e) => {
209
442
  if (e)
210
443
  this.handleRemoteParticipantsAudioLevel(e);
@@ -226,18 +459,83 @@ class Vapi extends VapiEventEmitter {
226
459
  });
227
460
  }
228
461
  });
229
- this.call.updateInputSettings({
230
- audio: {
231
- processor: {
232
- type: 'noise-cancellation',
462
+ // Stage 7: Audio processing setup
463
+ this.emit('call-start-progress', {
464
+ stage: 'audio-processing-setup',
465
+ status: 'started',
466
+ timestamp: new Date().toISOString()
467
+ });
468
+ const audioProcessingStartTime = Date.now();
469
+ try {
470
+ this.call.updateInputSettings({
471
+ audio: {
472
+ processor: {
473
+ type: 'noise-cancellation',
474
+ },
233
475
  },
234
- },
476
+ });
477
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
478
+ this.emit('call-start-progress', {
479
+ stage: 'audio-processing-setup',
480
+ status: 'completed',
481
+ duration: audioProcessingDuration,
482
+ timestamp: new Date().toISOString()
483
+ });
484
+ }
485
+ catch (error) {
486
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
487
+ this.emit('call-start-progress', {
488
+ stage: 'audio-processing-setup',
489
+ status: 'failed',
490
+ duration: audioProcessingDuration,
491
+ timestamp: new Date().toISOString(),
492
+ metadata: { error: error?.toString() }
493
+ });
494
+ this.emit('error', {
495
+ type: 'audio-processing-setup-error',
496
+ stage: 'audio-processing-setup',
497
+ error,
498
+ timestamp: new Date().toISOString()
499
+ });
500
+ // Don't throw here, this is non-critical
501
+ }
502
+ const totalDuration = Date.now() - startTime;
503
+ this.emit('call-start-success', {
504
+ totalDuration,
505
+ callId: webCall?.id || 'unknown',
506
+ timestamp: new Date().toISOString()
235
507
  });
236
508
  return webCall;
237
509
  }
238
510
  catch (e) {
239
- console.error(e);
240
- this.emit('error', e);
511
+ const totalDuration = Date.now() - startTime;
512
+ this.emit('call-start-failed', {
513
+ stage: 'unknown',
514
+ totalDuration,
515
+ error: e?.toString() || 'Unknown error occurred',
516
+ errorStack: e instanceof Error ? e.stack : 'No stack trace available',
517
+ timestamp: new Date().toISOString(),
518
+ context: {
519
+ hasAssistant: !!assistant,
520
+ hasSquad: !!squad,
521
+ hasWorkflow: !!workflow,
522
+ isMobile: this.isMobileDevice()
523
+ }
524
+ });
525
+ // Also emit the generic error event for backward compatibility
526
+ this.emit('error', {
527
+ type: 'start-method-error',
528
+ stage: 'unknown',
529
+ error: e,
530
+ totalDuration,
531
+ timestamp: new Date().toISOString(),
532
+ context: {
533
+ hasAssistant: !!assistant,
534
+ hasSquad: !!squad,
535
+ hasWorkflow: !!workflow,
536
+ isMobile: this.isMobileDevice()
537
+ }
538
+ });
241
539
  this.cleanup();
242
540
  return null;
243
541
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",
package/vapi.ts CHANGED
@@ -55,7 +55,36 @@ type VapiEventNames =
55
55
  | 'message'
56
56
  | 'video'
57
57
  | 'error'
58
- | 'daily-participant-updated';
58
+ | 'camera-error'
59
+ | 'network-quality-change'
60
+ | 'network-connection'
61
+ | 'daily-participant-updated'
62
+ | 'call-start-progress'
63
+ | 'call-start-success'
64
+ | 'call-start-failed';
65
+
66
+ interface CallStartProgressEvent {
67
+ stage: string;
68
+ status: 'started' | 'completed' | 'failed';
69
+ duration?: number;
70
+ timestamp: string;
71
+ metadata?: Record<string, any>;
72
+ }
73
+
74
+ interface CallStartSuccessEvent {
75
+ totalDuration: number;
76
+ callId?: string;
77
+ timestamp: string;
78
+ }
79
+
80
+ interface CallStartFailedEvent {
81
+ stage: string;
82
+ totalDuration: number;
83
+ error: string;
84
+ errorStack?: string;
85
+ timestamp: string;
86
+ context: Record<string, any>;
87
+ }
59
88
 
60
89
  type VapiEventListeners = {
61
90
  'call-end': () => void;
@@ -66,7 +95,13 @@ type VapiEventListeners = {
66
95
  video: (track: MediaStreamTrack) => void;
67
96
  message: (message: any) => void;
68
97
  error: (error: any) => void;
98
+ 'camera-error': (error: any) => void;
99
+ 'network-quality-change': (event: any) => void;
100
+ 'network-connection': (event: any) => void;
69
101
  'daily-participant-updated': (participant: DailyParticipant) => void;
102
+ 'call-start-progress': (event: CallStartProgressEvent) => void;
103
+ 'call-start-success': (event: CallStartSuccessEvent) => void;
104
+ 'call-start-failed': (event: CallStartFailedEvent) => void;
70
105
  };
71
106
 
72
107
  async function startAudioPlayer(
@@ -203,16 +238,53 @@ export default class Vapi extends VapiEventEmitter {
203
238
  workflow?: CreateWorkflowDTO | string,
204
239
  workflowOverrides?: WorkflowOverrides,
205
240
  ): Promise<Call | null> {
241
+ const startTime = Date.now();
242
+
243
+ // Input validation with detailed error messages
206
244
  if (!assistant && !squad && !workflow) {
207
- throw new Error('Assistant or Squad or Workflow must be provided.');
245
+ const error = new Error('Assistant or Squad or Workflow must be provided.');
246
+ this.emit('error', {
247
+ type: 'validation-error',
248
+ stage: 'input-validation',
249
+ message: error.message,
250
+ timestamp: new Date().toISOString()
251
+ });
252
+ throw error;
208
253
  }
209
254
 
210
255
  if (this.started) {
256
+ this.emit('call-start-progress', {
257
+ stage: 'validation',
258
+ status: 'failed',
259
+ timestamp: new Date().toISOString(),
260
+ metadata: { reason: 'already-started' }
261
+ });
211
262
  return null;
212
263
  }
264
+
265
+ this.emit('call-start-progress', {
266
+ stage: 'initialization',
267
+ status: 'started',
268
+ timestamp: new Date().toISOString(),
269
+ metadata: {
270
+ hasAssistant: !!assistant,
271
+ hasSquad: !!squad,
272
+ hasWorkflow: !!workflow
273
+ }
274
+ });
275
+
213
276
  this.started = true;
214
277
 
215
278
  try {
279
+ // Stage 1: Create web call
280
+ this.emit('call-start-progress', {
281
+ stage: 'web-call-creation',
282
+ status: 'started',
283
+ timestamp: new Date().toISOString()
284
+ });
285
+
286
+ const webCallStartTime = Date.now();
287
+
216
288
  const webCall = (
217
289
  await client.call.callControllerCreateWebCall({
218
290
  assistant: typeof assistant === 'string' ? undefined : assistant,
@@ -225,8 +297,27 @@ export default class Vapi extends VapiEventEmitter {
225
297
  workflowOverrides,
226
298
  })
227
299
  ).data;
300
+
301
+ const webCallDuration = Date.now() - webCallStartTime;
302
+ this.emit('call-start-progress', {
303
+ stage: 'web-call-creation',
304
+ status: 'completed',
305
+ duration: webCallDuration,
306
+ timestamp: new Date().toISOString(),
307
+ metadata: {
308
+ callId: webCall?.id || 'unknown',
309
+ videoRecordingEnabled: webCall?.artifactPlan?.videoRecordingEnabled ?? false,
310
+ voiceProvider: webCall?.assistant?.voice?.provider || 'unknown'
311
+ }
312
+ });
228
313
 
229
314
  if (this.call) {
315
+ this.emit('call-start-progress', {
316
+ stage: 'daily-call-object-creation',
317
+ status: 'started',
318
+ timestamp: new Date().toISOString(),
319
+ metadata: { action: 'cleanup-existing' }
320
+ });
230
321
  this.cleanup();
231
322
  }
232
323
 
@@ -235,11 +326,53 @@ export default class Vapi extends VapiEventEmitter {
235
326
 
236
327
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
237
328
 
238
- this.call = DailyIframe.createCallObject({
239
- audioSource: this.dailyCallObject.audioSource ?? true,
240
- videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
241
- dailyConfig: this.dailyCallConfig,
329
+ // Stage 2: Create Daily call object
330
+ this.emit('call-start-progress', {
331
+ stage: 'daily-call-object-creation',
332
+ status: 'started',
333
+ timestamp: new Date().toISOString(),
334
+ metadata: {
335
+ audioSource: this.dailyCallObject.audioSource ?? true,
336
+ videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
337
+ isVideoRecordingEnabled,
338
+ isVideoEnabled
339
+ }
242
340
  });
341
+
342
+ const dailyCallStartTime = Date.now();
343
+
344
+ try {
345
+ this.call = DailyIframe.createCallObject({
346
+ audioSource: this.dailyCallObject.audioSource ?? true,
347
+ videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
348
+ dailyConfig: this.dailyCallConfig,
349
+ });
350
+
351
+ const dailyCallDuration = Date.now() - dailyCallStartTime;
352
+ this.emit('call-start-progress', {
353
+ stage: 'daily-call-object-creation',
354
+ status: 'completed',
355
+ duration: dailyCallDuration,
356
+ timestamp: new Date().toISOString()
357
+ });
358
+ } catch (error) {
359
+ const dailyCallDuration = Date.now() - dailyCallStartTime;
360
+ this.emit('call-start-progress', {
361
+ stage: 'daily-call-object-creation',
362
+ status: 'failed',
363
+ duration: dailyCallDuration,
364
+ timestamp: new Date().toISOString(),
365
+ metadata: { error: error?.toString() }
366
+ });
367
+ this.emit('error', {
368
+ type: 'daily-call-object-creation-error',
369
+ stage: 'daily-call-object-creation',
370
+ error,
371
+ timestamp: new Date().toISOString()
372
+ });
373
+ throw error;
374
+ }
375
+
243
376
  this.call.iframe()?.style.setProperty('display', 'none');
244
377
 
245
378
  this.call.on('left-meeting', () => {
@@ -266,7 +399,15 @@ export default class Vapi extends VapiEventEmitter {
266
399
  });
267
400
 
268
401
  this.call.on('camera-error', (error: any) => {
269
- this.emit('error', error);
402
+ this.emit('camera-error', error);
403
+ });
404
+
405
+ this.call.on('network-quality-change', (event: any) => {
406
+ this.emit('network-quality-change', event);
407
+ });
408
+
409
+ this.call.on('network-connection', (event: any) => {
410
+ this.emit('network-connection', event);
270
411
  });
271
412
 
272
413
  this.call.on('track-started', async (e) => {
@@ -312,41 +453,182 @@ export default class Vapi extends VapiEventEmitter {
312
453
  destroyAudioPlayer(e.participant.session_id);
313
454
  });
314
455
 
315
- // Allow mobile devices to finish processing the microphone permissions
316
- // request before joining the call and playing the assistant's audio
317
- if (this.isMobileDevice()) {
456
+ // Stage 3: Mobile device handling and permissions
457
+ const isMobile = this.isMobileDevice();
458
+ this.emit('call-start-progress', {
459
+ stage: 'mobile-permissions',
460
+ status: 'started',
461
+ timestamp: new Date().toISOString(),
462
+ metadata: { isMobile }
463
+ });
464
+
465
+ if (isMobile) {
466
+ const mobileWaitStartTime = Date.now();
318
467
  await this.sleep(1000);
468
+ const mobileWaitDuration = Date.now() - mobileWaitStartTime;
469
+ this.emit('call-start-progress', {
470
+ stage: 'mobile-permissions',
471
+ status: 'completed',
472
+ duration: mobileWaitDuration,
473
+ timestamp: new Date().toISOString(),
474
+ metadata: { action: 'permissions-wait' }
475
+ });
476
+ } else {
477
+ this.emit('call-start-progress', {
478
+ stage: 'mobile-permissions',
479
+ status: 'completed',
480
+ timestamp: new Date().toISOString(),
481
+ metadata: { action: 'skipped-not-mobile' }
482
+ });
319
483
  }
320
484
 
321
- await this.call.join({
322
- // @ts-expect-error This exists
323
- url: webCall.webCallUrl,
324
- subscribeToTracksAutomatically: false,
485
+ // Stage 4: Join the call
486
+ this.emit('call-start-progress', {
487
+ stage: 'daily-call-join',
488
+ status: 'started',
489
+ timestamp: new Date().toISOString()
325
490
  });
491
+
492
+ const joinStartTime = Date.now();
493
+
494
+ try {
495
+ await this.call.join({
496
+ // @ts-expect-error This exists
497
+ url: webCall.webCallUrl,
498
+ subscribeToTracksAutomatically: false,
499
+ });
500
+
501
+ const joinDuration = Date.now() - joinStartTime;
502
+ this.emit('call-start-progress', {
503
+ stage: 'daily-call-join',
504
+ status: 'completed',
505
+ duration: joinDuration,
506
+ timestamp: new Date().toISOString()
507
+ });
508
+ } catch (error) {
509
+ const joinDuration = Date.now() - joinStartTime;
510
+ this.emit('call-start-progress', {
511
+ stage: 'daily-call-join',
512
+ status: 'failed',
513
+ duration: joinDuration,
514
+ timestamp: new Date().toISOString(),
515
+ metadata: { error: error?.toString() }
516
+ });
517
+ this.emit('error', {
518
+ type: 'daily-call-join-error',
519
+ stage: 'daily-call-join',
520
+ error,
521
+ duration: joinDuration,
522
+ timestamp: new Date().toISOString()
523
+ });
524
+ throw error;
525
+ }
326
526
 
527
+ // Stage 5: Video recording setup (if enabled)
327
528
  if (isVideoRecordingEnabled) {
529
+ this.emit('call-start-progress', {
530
+ stage: 'video-recording-setup',
531
+ status: 'started',
532
+ timestamp: new Date().toISOString()
533
+ });
534
+
328
535
  const recordingRequestedTime = new Date().getTime();
536
+ const recordingStartTime = Date.now();
329
537
 
330
- this.call.startRecording({
331
- width: 1280,
332
- height: 720,
333
- backgroundColor: '#FF1F2D3D',
334
- layout: {
335
- preset: 'default',
336
- },
337
- });
538
+ try {
539
+ this.call.startRecording({
540
+ width: 1280,
541
+ height: 720,
542
+ backgroundColor: '#FF1F2D3D',
543
+ layout: {
544
+ preset: 'default',
545
+ },
546
+ });
338
547
 
339
- this.call.on('recording-started', () => {
340
- this.send({
341
- type: 'control',
342
- control: 'say-first-message',
343
- videoRecordingStartDelaySeconds:
344
- (new Date().getTime() - recordingRequestedTime) / 1000,
548
+ const recordingSetupDuration = Date.now() - recordingStartTime;
549
+ this.emit('call-start-progress', {
550
+ stage: 'video-recording-setup',
551
+ status: 'completed',
552
+ duration: recordingSetupDuration,
553
+ timestamp: new Date().toISOString()
345
554
  });
555
+
556
+ this.call.on('recording-started', () => {
557
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
558
+ this.emit('call-start-progress', {
559
+ stage: 'video-recording-started',
560
+ status: 'completed',
561
+ timestamp: new Date().toISOString(),
562
+ metadata: { delaySeconds: totalRecordingDelay }
563
+ });
564
+
565
+ this.send({
566
+ type: 'control',
567
+ control: 'say-first-message',
568
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
569
+ });
570
+ });
571
+ } catch (error) {
572
+ const recordingSetupDuration = Date.now() - recordingStartTime;
573
+ this.emit('call-start-progress', {
574
+ stage: 'video-recording-setup',
575
+ status: 'failed',
576
+ duration: recordingSetupDuration,
577
+ timestamp: new Date().toISOString(),
578
+ metadata: { error: error?.toString() }
579
+ });
580
+ this.emit('error', {
581
+ type: 'video-recording-setup-error',
582
+ stage: 'video-recording-setup',
583
+ error,
584
+ timestamp: new Date().toISOString()
585
+ });
586
+ // Don't throw here, video recording is optional
587
+ }
588
+ } else {
589
+ this.emit('call-start-progress', {
590
+ stage: 'video-recording-setup',
591
+ status: 'completed',
592
+ timestamp: new Date().toISOString(),
593
+ metadata: { action: 'skipped-not-enabled' }
346
594
  });
347
595
  }
348
596
 
349
- this.call.startRemoteParticipantsAudioLevelObserver(100);
597
+ // Stage 6: Audio level observer setup
598
+ this.emit('call-start-progress', {
599
+ stage: 'audio-observer-setup',
600
+ status: 'started',
601
+ timestamp: new Date().toISOString()
602
+ });
603
+
604
+ const audioObserverStartTime = Date.now();
605
+
606
+ try {
607
+ this.call.startRemoteParticipantsAudioLevelObserver(100);
608
+ const audioObserverDuration = Date.now() - audioObserverStartTime;
609
+ this.emit('call-start-progress', {
610
+ stage: 'audio-observer-setup',
611
+ status: 'completed',
612
+ duration: audioObserverDuration,
613
+ timestamp: new Date().toISOString()
614
+ });
615
+ } catch (error) {
616
+ const audioObserverDuration = Date.now() - audioObserverStartTime;
617
+ this.emit('call-start-progress', {
618
+ stage: 'audio-observer-setup',
619
+ status: 'failed',
620
+ duration: audioObserverDuration,
621
+ timestamp: new Date().toISOString(),
622
+ metadata: { error: error?.toString() }
623
+ });
624
+ this.emit('error', {
625
+ type: 'audio-observer-setup-error',
626
+ stage: 'audio-observer-setup',
627
+ error,
628
+ timestamp: new Date().toISOString()
629
+ });
630
+ // Don't throw here, this is non-critical
631
+ }
350
632
 
351
633
  this.call.on('remote-participants-audio-level', (e) => {
352
634
  if (e) this.handleRemoteParticipantsAudioLevel(e);
@@ -371,18 +653,89 @@ export default class Vapi extends VapiEventEmitter {
371
653
  }
372
654
  });
373
655
 
374
- this.call.updateInputSettings({
375
- audio: {
376
- processor: {
377
- type: 'noise-cancellation',
656
+ // Stage 7: Audio processing setup
657
+ this.emit('call-start-progress', {
658
+ stage: 'audio-processing-setup',
659
+ status: 'started',
660
+ timestamp: new Date().toISOString()
661
+ });
662
+
663
+ const audioProcessingStartTime = Date.now();
664
+
665
+ try {
666
+ this.call.updateInputSettings({
667
+ audio: {
668
+ processor: {
669
+ type: 'noise-cancellation',
670
+ },
378
671
  },
379
- },
672
+ });
673
+
674
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
675
+ this.emit('call-start-progress', {
676
+ stage: 'audio-processing-setup',
677
+ status: 'completed',
678
+ duration: audioProcessingDuration,
679
+ timestamp: new Date().toISOString()
680
+ });
681
+ } catch (error) {
682
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
683
+ this.emit('call-start-progress', {
684
+ stage: 'audio-processing-setup',
685
+ status: 'failed',
686
+ duration: audioProcessingDuration,
687
+ timestamp: new Date().toISOString(),
688
+ metadata: { error: error?.toString() }
689
+ });
690
+ this.emit('error', {
691
+ type: 'audio-processing-setup-error',
692
+ stage: 'audio-processing-setup',
693
+ error,
694
+ timestamp: new Date().toISOString()
695
+ });
696
+ // Don't throw here, this is non-critical
697
+ }
698
+
699
+ const totalDuration = Date.now() - startTime;
700
+ this.emit('call-start-success', {
701
+ totalDuration,
702
+ callId: webCall?.id || 'unknown',
703
+ timestamp: new Date().toISOString()
380
704
  });
381
705
 
382
706
  return webCall;
383
707
  } catch (e) {
384
- console.error(e);
385
- this.emit('error', e);
708
+ const totalDuration = Date.now() - startTime;
709
+
710
+ this.emit('call-start-failed', {
711
+ stage: 'unknown',
712
+ totalDuration,
713
+ error: e?.toString() || 'Unknown error occurred',
714
+ errorStack: e instanceof Error ? e.stack : 'No stack trace available',
715
+ timestamp: new Date().toISOString(),
716
+ context: {
717
+ hasAssistant: !!assistant,
718
+ hasSquad: !!squad,
719
+ hasWorkflow: !!workflow,
720
+ isMobile: this.isMobileDevice()
721
+ }
722
+ });
723
+
724
+ // Also emit the generic error event for backward compatibility
725
+ this.emit('error', {
726
+ type: 'start-method-error',
727
+ stage: 'unknown',
728
+ error: e,
729
+ totalDuration,
730
+ timestamp: new Date().toISOString(),
731
+ context: {
732
+ hasAssistant: !!assistant,
733
+ hasSquad: !!squad,
734
+ hasWorkflow: !!workflow,
735
+ isMobile: this.isMobileDevice()
736
+ }
737
+ });
738
+
386
739
  this.cleanup();
387
740
  return null;
388
741
  }