@tvlabs/wdio-service 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -10,13 +10,17 @@
10
10
 
11
11
  ## Introduction
12
12
 
13
+ [![npm](https://img.shields.io/npm/v/@tvlabs/wdio-service)](https://www.npmjs.com/package/@tvlabs/wdio-service)
14
+
13
15
  The `@tvlabs/wdio-service` package uses a websocket to connect to the TV Labs platform before an Appium session begins, logging events relating to build upload and session creation as they occur. This offloads the responsibility of creating the TV Labs session from the `POST /session` Webdriver endpoint, leading to more reliable session requests and creation.
14
16
 
15
17
  If a build path is provided, the service first makes a build upload request to the TV Labs platform, and then sets the `tvlabs:build` capability to the newly created build ID.
16
18
 
17
19
  The service then makes a session request and then subscribes to events for that request. Once the session has been filled and is ready for the Webdriver script to begin, the service receives a ready event with the TV Labs session ID. This session ID is injected into the capabilities as `tvlabs:session_id` on the Webdriver session create request.
18
20
 
19
- Additionally, the service adds a unique request ID for each request made. The service will generate and attach an `x-request-id` header before each request to the TV Labs platform. This can be used to correlate requests in the client side logs to the Appium server logs.
21
+ Additionally, the service automatically injects an `Authorization` header with your API key (formatted as `Bearer ${apiKey}`) into all requests to the TV Labs platform. If you have already set an `Authorization` header in your configuration, the service will respect your existing header and not override it.
22
+
23
+ The service also adds a unique request ID for each request made. The service will generate and attach an `x-request-id` header before each request to the TV Labs platform. This can be used to correlate requests in the client side logs to the Appium server logs.
20
24
 
21
25
  ## Installation
22
26
 
@@ -64,17 +68,17 @@ async function run() {
64
68
  const wdOpts = {
65
69
  capabilities,
66
70
  hostname: 'appium.tvlabs.ai',
67
- port: 4723,
68
- headers: {
69
- Authorization: `Bearer ${process.env.TVLABS_API_TOKEN}`,
70
- },
71
+ port: 4723
71
72
  };
72
73
 
73
74
  const serviceOpts = {
74
75
  apiKey: process.env.TVLABS_API_TOKEN,
75
76
  }
76
77
 
77
- const service = new TVLabsService(serviceOpts, capabilities, {})
78
+ // NOTE: it is important to make sure that
79
+ // the wdOpts passed here are the same reference
80
+ // as the one passed to remote()
81
+ const service = new TVLabsService(serviceOpts, capabilities, wdOpts)
78
82
 
79
83
  // The TV Labs service does not use specs or a cid, pass default values.
80
84
  const cid = ""
@@ -141,3 +145,90 @@ run();
141
145
  - **Required:** No
142
146
  - **Default:** `false`
143
147
  - **Description:** Whether to continue the session request if any step fails. When `true`, the session request will still be made with the original provided capabilities. When `false`, the service will exit with a non-zero code.
148
+
149
+ ## Methods
150
+
151
+ ### `lastRequestId()`
152
+
153
+ - **Returns:** `string | undefined`
154
+ - **Description:** Returns the last request ID that was attached to a request made to the TV Labs platform. This is useful for correlating client-side logs with server-side logs. Returns `undefined` if no request has been made yet or if `attachRequestId` is set to `false`.
155
+
156
+ #### Example
157
+
158
+ ```javascript
159
+ import { remote } from 'webdriverio';
160
+ import { TVLabsService } from '@tvlabs/wdio-service';
161
+
162
+ const capabilities = { ... };
163
+ const wdOpts = { ... };
164
+
165
+ const service = new TVLabsService(
166
+ { apiKey: process.env.TVLABS_API_KEY },
167
+ capabilities,
168
+ wdOpts
169
+ );
170
+
171
+ await service.beforeSession(wdOpts, capabilities, [], '');
172
+
173
+ const driver = await remote(wdOpts);
174
+
175
+ // Get the last request ID
176
+ const requestId = service.lastRequestId();
177
+ console.log(`Last request ID: ${requestId}`);
178
+ ```
179
+
180
+ ### `requestMetadata()`
181
+
182
+ - **Parameters:** `appiumSessionId: string, requestIds: string | string[]`
183
+ - **Returns:** `Promise<TVLabsRequestMetadata | TVLabsRequestMetadataResponse>`
184
+ - **Description:** Fetches metadata for one or more Appium request IDs from the TV Labs platform. If a single request ID is provided, returns the metadata for that request. If an array of request IDs is provided, returns a map where keys are request IDs and values are their corresponding metadata.
185
+
186
+ > **Note:** Request metadata is processed asynchronously on the TV Labs platform. To ensure metadata is available, it is recommended to fetch request metadata a few seconds after the request, or after the session has ended.
187
+
188
+ #### Example
189
+
190
+ ```javascript
191
+ import { remote } from 'webdriverio';
192
+ import { TVLabsService } from '@tvlabs/wdio-service';
193
+
194
+ const capabilities = { ... };
195
+ const wdOpts = { ... };
196
+
197
+ const service = new TVLabsService(
198
+ { apiKey: process.env.TVLABS_API_KEY },
199
+ capabilities,
200
+ wdOpts
201
+ );
202
+
203
+ await service.beforeSession(wdOpts, capabilities, [], '');
204
+
205
+ const driver = await remote(wdOpts);
206
+ let requestId;
207
+
208
+ try {
209
+ // Perform some actions that generate requests
210
+ const element = await driver.$('#my-button');
211
+ await element.click();
212
+
213
+ // Get the request ID from the click
214
+ requestId = service.lastRequestId();
215
+ console.log(`Request ID: ${requestId}`);
216
+ } finally {
217
+ await driver.deleteSession();
218
+ }
219
+
220
+ // Fetch metadata after session ends (recommended)
221
+ if (requestId) {
222
+ const metadata = await service.requestMetadata(driver.sessionId, requestId);
223
+ console.log('Request metadata:', metadata);
224
+ }
225
+
226
+ // Fetch metadata for multiple requests
227
+ const multiMetadata = await service.requestMetadata("appium-session-id-1234"[
228
+ 'request-id-123',
229
+ 'request-id-456',
230
+ 'request-id-789'
231
+ ]);
232
+
233
+ console.log('Multiple requests metadata:', multiMetadata);
234
+ ```
@@ -0,0 +1,10 @@
1
+ import { BaseChannel } from './base.js';
2
+ import type { LogLevel, TVLabsRequestMetadataResponse } from '../types.js';
3
+ export declare class MetadataChannel extends BaseChannel {
4
+ private lobbyTopic;
5
+ constructor(endpoint: string, maxReconnectRetries: number, key: string, logLevel?: LogLevel);
6
+ disconnect(): Promise<void>;
7
+ connect(): Promise<void>;
8
+ getRequestMetadata(appiumSessionId: string, requestIds: string[]): Promise<TVLabsRequestMetadataResponse>;
9
+ }
10
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/channels/metadata.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAE3E,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,OAAO,CAAC,UAAU,CAAU;gBAG1B,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAYvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBxB,kBAAkB,CACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,6BAA6B,CAAC;CAyB1C"}
package/cjs/index.js CHANGED
@@ -11,20 +11,20 @@ var path = require('node:path');
11
11
  var crypto = require('node:crypto');
12
12
 
13
13
  function _interopNamespaceDefault(e) {
14
- var n = Object.create(null);
15
- if (e) {
16
- Object.keys(e).forEach(function (k) {
17
- if (k !== 'default') {
18
- var d = Object.getOwnPropertyDescriptor(e, k);
19
- Object.defineProperty(n, k, d.get ? d : {
20
- enumerable: true,
21
- get: function () { return e[k]; }
22
- });
23
- }
14
+ var n = Object.create(null);
15
+ if (e) {
16
+ Object.keys(e).forEach(function (k) {
17
+ if (k !== 'default') {
18
+ var d = Object.getOwnPropertyDescriptor(e, k);
19
+ Object.defineProperty(n, k, d.get ? d : {
20
+ enumerable: true,
21
+ get: function () { return e[k]; }
24
22
  });
25
- }
26
- n.default = e;
27
- return Object.freeze(n);
23
+ }
24
+ });
25
+ }
26
+ n.default = e;
27
+ return Object.freeze(n);
28
28
  }
29
29
 
30
30
  var crypto__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(crypto$1);
@@ -32,6 +32,25 @@ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
32
32
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
33
33
  var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
34
34
 
35
+ var name = "@tvlabs/wdio-service";
36
+ var version = "0.1.10";
37
+ var packageJson = {
38
+ name: name,
39
+ version: version};
40
+
41
+ function getServiceInfo() {
42
+ return {
43
+ service_name: getServiceName(),
44
+ service_version: getServiceVersion(),
45
+ };
46
+ }
47
+ function getServiceVersion() {
48
+ return packageJson.version;
49
+ }
50
+ function getServiceName() {
51
+ return packageJson.name;
52
+ }
53
+
35
54
  const LOG_LEVELS = {
36
55
  error: 0,
37
56
  warn: 1,
@@ -135,25 +154,6 @@ class Logger {
135
154
  }
136
155
  }
137
156
 
138
- var name = "@tvlabs/wdio-service";
139
- var version = "0.1.8";
140
- var packageJson = {
141
- name: name,
142
- version: version};
143
-
144
- function getServiceInfo() {
145
- return {
146
- service_name: getServiceName(),
147
- service_version: getServiceVersion(),
148
- };
149
- }
150
- function getServiceVersion() {
151
- return packageJson.version;
152
- }
153
- function getServiceName() {
154
- return packageJson.name;
155
- }
156
-
157
157
  class BaseChannel {
158
158
  endpoint;
159
159
  maxReconnectRetries;
@@ -448,19 +448,82 @@ class BuildChannel extends BaseChannel {
448
448
  }
449
449
  }
450
450
 
451
+ class MetadataChannel extends BaseChannel {
452
+ lobbyTopic;
453
+ constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
454
+ super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/metadata-channel');
455
+ this.lobbyTopic = this.socket.channel('metadata:lobby');
456
+ }
457
+ async disconnect() {
458
+ return new Promise((res, _rej) => {
459
+ this.lobbyTopic.leave();
460
+ this.socket.disconnect(() => res());
461
+ });
462
+ }
463
+ async connect() {
464
+ try {
465
+ this.log.debug('Connecting to metadata channel...');
466
+ this.socket.connect();
467
+ await this.join(this.lobbyTopic);
468
+ this.log.debug('Connected to metadata channel!');
469
+ }
470
+ catch (error) {
471
+ this.log.error('Error connecting to metadata channel:', error);
472
+ throw new webdriverio.SevereServiceError('Could not connect to metadata channel, please check your connection.');
473
+ }
474
+ }
475
+ async getRequestMetadata(appiumSessionId, requestIds) {
476
+ this.log.debug(`Fetching metadata for session ${appiumSessionId}, requests: ${requestIds.join(', ')}`);
477
+ try {
478
+ const response = await this.push(this.lobbyTopic, 'appium_request_metadata', {
479
+ appium_session_id: appiumSessionId,
480
+ request_ids: requestIds,
481
+ });
482
+ this.log.debug(`Received metadata for ${Object.keys(response).length} request(s)`);
483
+ return response;
484
+ }
485
+ catch (error) {
486
+ this.log.error('Error fetching request metadata:', error);
487
+ throw error;
488
+ }
489
+ }
490
+ }
491
+
451
492
  class TVLabsService {
452
493
  _options;
453
494
  _capabilities;
454
495
  _config;
455
496
  log;
497
+ requestId;
498
+ sessionId;
499
+ metadataChannel;
456
500
  constructor(_options, _capabilities, _config) {
457
501
  this._options = _options;
458
502
  this._capabilities = _capabilities;
459
503
  this._config = _config;
460
504
  this.log = new Logger('@tvlabs/wdio-service', this._config.logLevel);
505
+ this.injectAuthorizationHeader();
461
506
  if (this.attachRequestId()) {
462
507
  this.setupRequestId();
463
508
  }
509
+ this.log.info(`Instantiated TVLabsService v${getServiceVersion()}`);
510
+ }
511
+ lastRequestId() {
512
+ return this.requestId;
513
+ }
514
+ async requestMetadata(sessionId, requestIds) {
515
+ const requestIdArray = Array.isArray(requestIds)
516
+ ? requestIds
517
+ : [requestIds];
518
+ if (!this.metadataChannel) {
519
+ this.metadataChannel = new MetadataChannel(this.sessionEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
520
+ await this.metadataChannel.connect();
521
+ }
522
+ const response = await this.metadataChannel.getRequestMetadata(sessionId, requestIdArray);
523
+ if (!Array.isArray(requestIds)) {
524
+ return response[requestIds];
525
+ }
526
+ return response;
464
527
  }
465
528
  onPrepare(_config, param) {
466
529
  try {
@@ -496,6 +559,12 @@ class TVLabsService {
496
559
  throw error;
497
560
  }
498
561
  }
562
+ injectAuthorizationHeader() {
563
+ this._config.headers = this._config.headers || {};
564
+ if (!this._config.headers.Authorization) {
565
+ this._config.headers.Authorization = `Bearer ${this.apiKey()}`;
566
+ }
567
+ }
499
568
  setupRequestId() {
500
569
  const originalTransformRequest = this._config.transformRequest;
501
570
  this._config.transformRequest = (requestOptions) => {
@@ -507,7 +576,8 @@ class TVLabsService {
507
576
  originalRequestOptions.headers = {};
508
577
  }
509
578
  this.setRequestHeader(originalRequestOptions.headers, 'x-request-id', requestId);
510
- this.log.info('ATTACHED REQUEST ID', requestId);
579
+ this.log.debug('ATTACHED REQUEST ID', requestId);
580
+ this.setRequestId(requestId);
511
581
  return originalRequestOptions;
512
582
  };
513
583
  }
@@ -524,6 +594,9 @@ class TVLabsService {
524
594
  }
525
595
  }
526
596
  }
597
+ setRequestId(id) {
598
+ this.requestId = id;
599
+ }
527
600
  continueOnError() {
528
601
  return this._options.continueOnError ?? false;
529
602
  }
package/cjs/service.d.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  import type { Services, Capabilities, Options } from '@wdio/types';
2
- import type { TVLabsCapabilities, TVLabsServiceOptions } from './types.js';
2
+ import type { TVLabsCapabilities, TVLabsServiceOptions, TVLabsRequestMetadata, TVLabsRequestMetadataResponse } from './types.js';
3
3
  export default class TVLabsService implements Services.ServiceInstance {
4
4
  private _options;
5
5
  private _capabilities;
6
6
  private _config;
7
7
  private log;
8
+ private requestId;
9
+ private sessionId;
10
+ private metadataChannel;
8
11
  constructor(_options: TVLabsServiceOptions, _capabilities: Capabilities.ResolvedTestrunnerCapabilities, _config: Options.WebdriverIO);
12
+ lastRequestId(): string | undefined;
13
+ requestMetadata(sessionId: string, requestIds: string | string[]): Promise<TVLabsRequestMetadata | TVLabsRequestMetadataResponse>;
9
14
  onPrepare(_config: Options.Testrunner, param: Capabilities.TestrunnerCapabilities): void;
10
15
  beforeSession(_config: Omit<Options.Testrunner, 'capabilities'>, capabilities: TVLabsCapabilities, _specs: string[], _cid: string): Promise<void>;
16
+ private injectAuthorizationHeader;
11
17
  private setupRequestId;
12
18
  private setRequestHeader;
19
+ private setRequestId;
13
20
  private continueOnError;
14
21
  private buildPath;
15
22
  private appSlug;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAIlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,GAAG,CAAS;gBAGV,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAQtC,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAiBtC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IA+Cd,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,6BAA6B,EAE9B,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAOlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IARjB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,eAAe,CAA8B;gBAG3C,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAatC,aAAa,IAAI,MAAM,GAAG,SAAS;IAI7B,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAC5B,OAAO,CAAC,qBAAqB,GAAG,6BAA6B,CAAC;IA+BjE,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAiBtC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IA+Cd,OAAO,CAAC,yBAAyB;IAQjC,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
package/cjs/types.d.ts CHANGED
@@ -32,8 +32,20 @@ export type TVLabsSessionRequestUpdate = {
32
32
  session_id: string;
33
33
  reason: string;
34
34
  };
35
+ export type ResponseAnyValue = string | number | boolean | null | ResponseAnyValue[] | {
36
+ [key: string]: ResponseAnyValue;
37
+ };
35
38
  export type TVLabsSessionRequestResponse = {
39
+ status: number;
40
+ path: string;
36
41
  request_id: string;
42
+ method: string;
43
+ req_body: ResponseAnyValue;
44
+ resp_body: ResponseAnyValue;
45
+ requested_at: string | null;
46
+ responded_at: string | null;
47
+ video_start_time: number | null;
48
+ video_end_time: number | null;
37
49
  };
38
50
  export type TVLabsSocketParams = TVLabsServiceInfo & {
39
51
  api_key: string;
@@ -57,4 +69,10 @@ export type TVLabsBuildMetadata = {
57
69
  size: number;
58
70
  sha256: string;
59
71
  };
72
+ export type TVLabsRequestMetadata = {
73
+ [key: string]: unknown;
74
+ };
75
+ export type TVLabsRequestMetadataResponse = {
76
+ [request_id: string]: TVLabsRequestMetadata;
77
+ };
60
78
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC5B,YAAY,CAAC,+BAA+B,GAAG;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kCAAkC,CAAC,EAAE,MAAM,CAAC;QAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEJ,MAAM,MAAM,gCAAgC,GAAG,CAC7C,QAAQ,EAAE,0BAA0B,KACjC,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { BaseChannel } from './base.js';
2
+ import type { LogLevel, TVLabsRequestMetadataResponse } from '../types.js';
3
+ export declare class MetadataChannel extends BaseChannel {
4
+ private lobbyTopic;
5
+ constructor(endpoint: string, maxReconnectRetries: number, key: string, logLevel?: LogLevel);
6
+ disconnect(): Promise<void>;
7
+ connect(): Promise<void>;
8
+ getRequestMetadata(appiumSessionId: string, requestIds: string[]): Promise<TVLabsRequestMetadataResponse>;
9
+ }
10
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/channels/metadata.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAE3E,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,OAAO,CAAC,UAAU,CAAU;gBAG1B,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAYvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBxB,kBAAkB,CACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,6BAA6B,CAAC;CAyB1C"}
package/esm/index.js CHANGED
@@ -6,6 +6,25 @@ import * as fs from 'node:fs';
6
6
  import * as path from 'node:path';
7
7
  import * as crypto from 'node:crypto';
8
8
 
9
+ var name = "@tvlabs/wdio-service";
10
+ var version = "0.1.10";
11
+ var packageJson = {
12
+ name: name,
13
+ version: version};
14
+
15
+ function getServiceInfo() {
16
+ return {
17
+ service_name: getServiceName(),
18
+ service_version: getServiceVersion(),
19
+ };
20
+ }
21
+ function getServiceVersion() {
22
+ return packageJson.version;
23
+ }
24
+ function getServiceName() {
25
+ return packageJson.name;
26
+ }
27
+
9
28
  const LOG_LEVELS = {
10
29
  error: 0,
11
30
  warn: 1,
@@ -109,25 +128,6 @@ class Logger {
109
128
  }
110
129
  }
111
130
 
112
- var name = "@tvlabs/wdio-service";
113
- var version = "0.1.8";
114
- var packageJson = {
115
- name: name,
116
- version: version};
117
-
118
- function getServiceInfo() {
119
- return {
120
- service_name: getServiceName(),
121
- service_version: getServiceVersion(),
122
- };
123
- }
124
- function getServiceVersion() {
125
- return packageJson.version;
126
- }
127
- function getServiceName() {
128
- return packageJson.name;
129
- }
130
-
131
131
  class BaseChannel {
132
132
  endpoint;
133
133
  maxReconnectRetries;
@@ -422,19 +422,82 @@ class BuildChannel extends BaseChannel {
422
422
  }
423
423
  }
424
424
 
425
+ class MetadataChannel extends BaseChannel {
426
+ lobbyTopic;
427
+ constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
428
+ super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/metadata-channel');
429
+ this.lobbyTopic = this.socket.channel('metadata:lobby');
430
+ }
431
+ async disconnect() {
432
+ return new Promise((res, _rej) => {
433
+ this.lobbyTopic.leave();
434
+ this.socket.disconnect(() => res());
435
+ });
436
+ }
437
+ async connect() {
438
+ try {
439
+ this.log.debug('Connecting to metadata channel...');
440
+ this.socket.connect();
441
+ await this.join(this.lobbyTopic);
442
+ this.log.debug('Connected to metadata channel!');
443
+ }
444
+ catch (error) {
445
+ this.log.error('Error connecting to metadata channel:', error);
446
+ throw new SevereServiceError('Could not connect to metadata channel, please check your connection.');
447
+ }
448
+ }
449
+ async getRequestMetadata(appiumSessionId, requestIds) {
450
+ this.log.debug(`Fetching metadata for session ${appiumSessionId}, requests: ${requestIds.join(', ')}`);
451
+ try {
452
+ const response = await this.push(this.lobbyTopic, 'appium_request_metadata', {
453
+ appium_session_id: appiumSessionId,
454
+ request_ids: requestIds,
455
+ });
456
+ this.log.debug(`Received metadata for ${Object.keys(response).length} request(s)`);
457
+ return response;
458
+ }
459
+ catch (error) {
460
+ this.log.error('Error fetching request metadata:', error);
461
+ throw error;
462
+ }
463
+ }
464
+ }
465
+
425
466
  class TVLabsService {
426
467
  _options;
427
468
  _capabilities;
428
469
  _config;
429
470
  log;
471
+ requestId;
472
+ sessionId;
473
+ metadataChannel;
430
474
  constructor(_options, _capabilities, _config) {
431
475
  this._options = _options;
432
476
  this._capabilities = _capabilities;
433
477
  this._config = _config;
434
478
  this.log = new Logger('@tvlabs/wdio-service', this._config.logLevel);
479
+ this.injectAuthorizationHeader();
435
480
  if (this.attachRequestId()) {
436
481
  this.setupRequestId();
437
482
  }
483
+ this.log.info(`Instantiated TVLabsService v${getServiceVersion()}`);
484
+ }
485
+ lastRequestId() {
486
+ return this.requestId;
487
+ }
488
+ async requestMetadata(sessionId, requestIds) {
489
+ const requestIdArray = Array.isArray(requestIds)
490
+ ? requestIds
491
+ : [requestIds];
492
+ if (!this.metadataChannel) {
493
+ this.metadataChannel = new MetadataChannel(this.sessionEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
494
+ await this.metadataChannel.connect();
495
+ }
496
+ const response = await this.metadataChannel.getRequestMetadata(sessionId, requestIdArray);
497
+ if (!Array.isArray(requestIds)) {
498
+ return response[requestIds];
499
+ }
500
+ return response;
438
501
  }
439
502
  onPrepare(_config, param) {
440
503
  try {
@@ -470,6 +533,12 @@ class TVLabsService {
470
533
  throw error;
471
534
  }
472
535
  }
536
+ injectAuthorizationHeader() {
537
+ this._config.headers = this._config.headers || {};
538
+ if (!this._config.headers.Authorization) {
539
+ this._config.headers.Authorization = `Bearer ${this.apiKey()}`;
540
+ }
541
+ }
473
542
  setupRequestId() {
474
543
  const originalTransformRequest = this._config.transformRequest;
475
544
  this._config.transformRequest = (requestOptions) => {
@@ -481,7 +550,8 @@ class TVLabsService {
481
550
  originalRequestOptions.headers = {};
482
551
  }
483
552
  this.setRequestHeader(originalRequestOptions.headers, 'x-request-id', requestId);
484
- this.log.info('ATTACHED REQUEST ID', requestId);
553
+ this.log.debug('ATTACHED REQUEST ID', requestId);
554
+ this.setRequestId(requestId);
485
555
  return originalRequestOptions;
486
556
  };
487
557
  }
@@ -498,6 +568,9 @@ class TVLabsService {
498
568
  }
499
569
  }
500
570
  }
571
+ setRequestId(id) {
572
+ this.requestId = id;
573
+ }
501
574
  continueOnError() {
502
575
  return this._options.continueOnError ?? false;
503
576
  }
package/esm/service.d.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  import type { Services, Capabilities, Options } from '@wdio/types';
2
- import type { TVLabsCapabilities, TVLabsServiceOptions } from './types.js';
2
+ import type { TVLabsCapabilities, TVLabsServiceOptions, TVLabsRequestMetadata, TVLabsRequestMetadataResponse } from './types.js';
3
3
  export default class TVLabsService implements Services.ServiceInstance {
4
4
  private _options;
5
5
  private _capabilities;
6
6
  private _config;
7
7
  private log;
8
+ private requestId;
9
+ private sessionId;
10
+ private metadataChannel;
8
11
  constructor(_options: TVLabsServiceOptions, _capabilities: Capabilities.ResolvedTestrunnerCapabilities, _config: Options.WebdriverIO);
12
+ lastRequestId(): string | undefined;
13
+ requestMetadata(sessionId: string, requestIds: string | string[]): Promise<TVLabsRequestMetadata | TVLabsRequestMetadataResponse>;
9
14
  onPrepare(_config: Options.Testrunner, param: Capabilities.TestrunnerCapabilities): void;
10
15
  beforeSession(_config: Omit<Options.Testrunner, 'capabilities'>, capabilities: TVLabsCapabilities, _specs: string[], _cid: string): Promise<void>;
16
+ private injectAuthorizationHeader;
11
17
  private setupRequestId;
12
18
  private setRequestHeader;
19
+ private setRequestId;
13
20
  private continueOnError;
14
21
  private buildPath;
15
22
  private appSlug;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAIlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,GAAG,CAAS;gBAGV,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAQtC,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAiBtC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IA+Cd,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,6BAA6B,EAE9B,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAOlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IARjB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,eAAe,CAA8B;gBAG3C,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAatC,aAAa,IAAI,MAAM,GAAG,SAAS;IAI7B,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAC5B,OAAO,CAAC,qBAAqB,GAAG,6BAA6B,CAAC;IA+BjE,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAiBtC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IA+Cd,OAAO,CAAC,yBAAyB;IAQjC,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
package/esm/types.d.ts CHANGED
@@ -32,8 +32,20 @@ export type TVLabsSessionRequestUpdate = {
32
32
  session_id: string;
33
33
  reason: string;
34
34
  };
35
+ export type ResponseAnyValue = string | number | boolean | null | ResponseAnyValue[] | {
36
+ [key: string]: ResponseAnyValue;
37
+ };
35
38
  export type TVLabsSessionRequestResponse = {
39
+ status: number;
40
+ path: string;
36
41
  request_id: string;
42
+ method: string;
43
+ req_body: ResponseAnyValue;
44
+ resp_body: ResponseAnyValue;
45
+ requested_at: string | null;
46
+ responded_at: string | null;
47
+ video_start_time: number | null;
48
+ video_end_time: number | null;
37
49
  };
38
50
  export type TVLabsSocketParams = TVLabsServiceInfo & {
39
51
  api_key: string;
@@ -57,4 +69,10 @@ export type TVLabsBuildMetadata = {
57
69
  size: number;
58
70
  sha256: string;
59
71
  };
72
+ export type TVLabsRequestMetadata = {
73
+ [key: string]: unknown;
74
+ };
75
+ export type TVLabsRequestMetadataResponse = {
76
+ [request_id: string]: TVLabsRequestMetadata;
77
+ };
60
78
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC5B,YAAY,CAAC,+BAA+B,GAAG;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kCAAkC,CAAC,EAAE,MAAM,CAAC;QAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEJ,MAAM,MAAM,gCAAgC,GAAG,CAC7C,QAAQ,EAAE,0BAA0B,KACjC,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tvlabs/wdio-service",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "WebdriverIO service that provides a better integration into TV Labs",
5
5
  "author": "Regan Karlewicz <regan@tvlabs.ai>",
6
6
  "license": "Apache-2.0",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "npm run clean && rollup -c",
26
- "start": "ts-node src/index.ts",
26
+ "start": "tsx --env-file=.env scripts/test.ts",
27
27
  "dev": "ts-node --transpile-only src/index.ts",
28
28
  "clean": "rm -rf cjs && rm -rf esm",
29
29
  "format": "prettier --write .",
@@ -31,6 +31,9 @@
31
31
  "lint": "eslint",
32
32
  "test": "vitest --config vitest.config.ts --coverage --run",
33
33
  "test:watch": "vitest --config vitest.config.ts --coverage",
34
+ "test:integration": "npm run build && npm run test:integration:esm && npm run test:integration:cjs",
35
+ "test:integration:esm": "vitest --config vitest.config.esm.ts --run",
36
+ "test:integration:cjs": "vitest --config vitest.config.cjs.ts --run",
34
37
  "publish:dry": "npm publish --access public --provenance --dry-run"
35
38
  },
36
39
  "type": "module",
@@ -43,7 +46,7 @@
43
46
  "typeScriptVersion": "5.8.2",
44
47
  "devDependencies": {
45
48
  "@eslint/js": "^9.22.0",
46
- "@rollup/plugin-commonjs": "^28.0.6",
49
+ "@rollup/plugin-commonjs": "^29.0.0",
47
50
  "@rollup/plugin-json": "^6.1.0",
48
51
  "@rollup/plugin-node-resolve": "^16.0.1",
49
52
  "@rollup/plugin-typescript": "^12.1.4",
@@ -52,7 +55,7 @@
52
55
  "@types/ws": "^8.18.0",
53
56
  "@typescript-eslint/eslint-plugin": "^8.38.0",
54
57
  "@typescript-eslint/parser": "^8.38.0",
55
- "@vitest/coverage-v8": "^3.0.9",
58
+ "@vitest/coverage-v8": "^4.0.7",
56
59
  "@wdio/globals": "^9.12.1",
57
60
  "@wdio/types": "^9.10.1",
58
61
  "eslint": "^9.30.1",
@@ -60,9 +63,10 @@
60
63
  "jiti": "^2.4.2",
61
64
  "prettier": "^3.5.3",
62
65
  "rollup": "^4.45.1",
63
- "typescript": "^5.8.2",
66
+ "tsx": "^4.20.6",
67
+ "typescript": "^5.9.3",
64
68
  "typescript-eslint": "^8.38.0",
65
- "vitest": "^3.0.9",
69
+ "vitest": "^4.0.7",
66
70
  "webdriverio": "^9.12.1"
67
71
  },
68
72
  "dependencies": {
@@ -0,0 +1,65 @@
1
+ import { remote } from 'webdriverio';
2
+ import { TVLabsService } from '../src/index.ts';
3
+
4
+ const apiKey = process.env.TVLABS_API_TOKEN;
5
+
6
+ const capabilities = {
7
+ 'tvlabs:build': '00000000-0000-0000-0000-000000000000',
8
+ 'tvlabs:teleport_region': 'fake',
9
+ 'tvlabs:constraints': 'platform_key:dev AND model:"Virtual Device"',
10
+ };
11
+
12
+ const wdOpts = {
13
+ hostname: 'localhost',
14
+ port: 4723, // Appium proxy
15
+ logLevel: 'info',
16
+ capabilities,
17
+ };
18
+
19
+ const serviceOpts = {
20
+ apiKey,
21
+ sessionEndpoint: 'ws://localhost:4000/appium',
22
+ };
23
+
24
+ const service = new TVLabsService(serviceOpts, capabilities, wdOpts);
25
+
26
+ async function runTest() {
27
+ await service.beforeSession(wdOpts, capabilities, [], '');
28
+ const driver = await remote(wdOpts);
29
+
30
+ console.log('The session id is: ', driver.sessionId);
31
+
32
+ const requests = [];
33
+
34
+ try {
35
+ for (let i = 0; i < 3; ++i) {
36
+ await driver.execute('fake: getThing', {
37
+ thing: 'thing',
38
+ });
39
+
40
+ requests.push(service.lastRequestId());
41
+ }
42
+ } finally {
43
+ await driver.pause(1000);
44
+ await driver.deleteSession();
45
+ }
46
+
47
+ const allRequests = await service.requestMetadata(driver.sessionId, requests);
48
+ const singleRequest = await service.requestMetadata(
49
+ driver.sessionId,
50
+ requests[0],
51
+ );
52
+
53
+ console.log('Single request', singleRequest);
54
+ console.log('All requests', allRequests);
55
+ }
56
+
57
+ runTest()
58
+ .then(() => {
59
+ console.log('Test completed successfully');
60
+ process.exit(0);
61
+ })
62
+ .catch((error) => {
63
+ console.error('Test failed:', error);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,78 @@
1
+ import { type Channel } from 'phoenix';
2
+ import { SevereServiceError } from 'webdriverio';
3
+ import { BaseChannel } from './base.js';
4
+
5
+ import type { LogLevel, TVLabsRequestMetadataResponse } from '../types.js';
6
+
7
+ export class MetadataChannel extends BaseChannel {
8
+ private lobbyTopic: Channel;
9
+
10
+ constructor(
11
+ endpoint: string,
12
+ maxReconnectRetries: number,
13
+ key: string,
14
+ logLevel: LogLevel = 'info',
15
+ ) {
16
+ super(
17
+ endpoint,
18
+ maxReconnectRetries,
19
+ key,
20
+ logLevel,
21
+ '@tvlabs/metadata-channel',
22
+ );
23
+ this.lobbyTopic = this.socket.channel('metadata:lobby');
24
+ }
25
+
26
+ async disconnect(): Promise<void> {
27
+ return new Promise((res, _rej) => {
28
+ this.lobbyTopic.leave();
29
+ this.socket.disconnect(() => res());
30
+ });
31
+ }
32
+
33
+ async connect(): Promise<void> {
34
+ try {
35
+ this.log.debug('Connecting to metadata channel...');
36
+
37
+ this.socket.connect();
38
+
39
+ await this.join(this.lobbyTopic);
40
+
41
+ this.log.debug('Connected to metadata channel!');
42
+ } catch (error) {
43
+ this.log.error('Error connecting to metadata channel:', error);
44
+ throw new SevereServiceError(
45
+ 'Could not connect to metadata channel, please check your connection.',
46
+ );
47
+ }
48
+ }
49
+
50
+ async getRequestMetadata(
51
+ appiumSessionId: string,
52
+ requestIds: string[],
53
+ ): Promise<TVLabsRequestMetadataResponse> {
54
+ this.log.debug(
55
+ `Fetching metadata for session ${appiumSessionId}, requests: ${requestIds.join(', ')}`,
56
+ );
57
+
58
+ try {
59
+ const response = await this.push<TVLabsRequestMetadataResponse>(
60
+ this.lobbyTopic,
61
+ 'appium_request_metadata',
62
+ {
63
+ appium_session_id: appiumSessionId,
64
+ request_ids: requestIds,
65
+ },
66
+ );
67
+
68
+ this.log.debug(
69
+ `Received metadata for ${Object.keys(response).length} request(s)`,
70
+ );
71
+
72
+ return response;
73
+ } catch (error) {
74
+ this.log.error('Error fetching request metadata:', error);
75
+ throw error;
76
+ }
77
+ }
78
+ }
package/src/service.ts CHANGED
@@ -1,19 +1,26 @@
1
1
  import { SevereServiceError } from 'webdriverio';
2
2
  import * as crypto from 'crypto';
3
+ import { getServiceVersion } from './utils.js';
3
4
 
4
5
  import { SessionChannel } from './channels/session.js';
5
6
  import { BuildChannel } from './channels/build.js';
7
+ import { MetadataChannel } from './channels/metadata.js';
6
8
  import { Logger } from './logger.js';
7
9
 
8
10
  import type { Services, Capabilities, Options } from '@wdio/types';
9
11
  import type {
10
12
  TVLabsCapabilities,
11
13
  TVLabsServiceOptions,
14
+ TVLabsRequestMetadata,
15
+ TVLabsRequestMetadataResponse,
12
16
  LogLevel,
13
17
  } from './types.js';
14
18
 
15
19
  export default class TVLabsService implements Services.ServiceInstance {
16
20
  private log: Logger;
21
+ private requestId: string | undefined;
22
+ private sessionId: string | undefined;
23
+ private metadataChannel: MetadataChannel | undefined;
17
24
 
18
25
  constructor(
19
26
  private _options: TVLabsServiceOptions,
@@ -21,9 +28,52 @@ export default class TVLabsService implements Services.ServiceInstance {
21
28
  private _config: Options.WebdriverIO,
22
29
  ) {
23
30
  this.log = new Logger('@tvlabs/wdio-service', this._config.logLevel);
31
+
32
+ this.injectAuthorizationHeader();
33
+
24
34
  if (this.attachRequestId()) {
25
35
  this.setupRequestId();
26
36
  }
37
+
38
+ this.log.info(`Instantiated TVLabsService v${getServiceVersion()}`);
39
+ }
40
+
41
+ lastRequestId(): string | undefined {
42
+ return this.requestId;
43
+ }
44
+
45
+ async requestMetadata(
46
+ sessionId: string,
47
+ requestIds: string | string[],
48
+ ): Promise<TVLabsRequestMetadata | TVLabsRequestMetadataResponse> {
49
+ const requestIdArray = Array.isArray(requestIds)
50
+ ? requestIds
51
+ : [requestIds];
52
+
53
+ // Create and connect to metadata channel if not already connected
54
+ if (!this.metadataChannel) {
55
+ this.metadataChannel = new MetadataChannel(
56
+ this.sessionEndpoint(),
57
+ this.reconnectRetries(),
58
+ this.apiKey(),
59
+ this.logLevel(),
60
+ );
61
+
62
+ await this.metadataChannel.connect();
63
+ }
64
+
65
+ const response = await this.metadataChannel.getRequestMetadata(
66
+ sessionId,
67
+ requestIdArray,
68
+ );
69
+
70
+ // If a single request ID was passed, return just that request's metadata
71
+ if (!Array.isArray(requestIds)) {
72
+ return response[requestIds];
73
+ }
74
+
75
+ // Otherwise return the full map
76
+ return response;
27
77
  }
28
78
 
29
79
  onPrepare(
@@ -96,6 +146,14 @@ export default class TVLabsService implements Services.ServiceInstance {
96
146
  }
97
147
  }
98
148
 
149
+ private injectAuthorizationHeader() {
150
+ this._config.headers = this._config.headers || {};
151
+
152
+ if (!this._config.headers.Authorization) {
153
+ this._config.headers.Authorization = `Bearer ${this.apiKey()}`;
154
+ }
155
+ }
156
+
99
157
  private setupRequestId() {
100
158
  const originalTransformRequest = this._config.transformRequest;
101
159
 
@@ -116,7 +174,9 @@ export default class TVLabsService implements Services.ServiceInstance {
116
174
  requestId,
117
175
  );
118
176
 
119
- this.log.info('ATTACHED REQUEST ID', requestId);
177
+ this.log.debug('ATTACHED REQUEST ID', requestId);
178
+
179
+ this.setRequestId(requestId);
120
180
 
121
181
  return originalRequestOptions;
122
182
  };
@@ -138,6 +198,10 @@ export default class TVLabsService implements Services.ServiceInstance {
138
198
  }
139
199
  }
140
200
 
201
+ private setRequestId(id: string) {
202
+ this.requestId = id;
203
+ }
204
+
141
205
  private continueOnError(): boolean {
142
206
  return this._options.continueOnError ?? false;
143
207
  }
package/src/types.ts CHANGED
@@ -41,8 +41,25 @@ export type TVLabsSessionRequestUpdate = {
41
41
  reason: string;
42
42
  };
43
43
 
44
+ export type ResponseAnyValue =
45
+ | string
46
+ | number
47
+ | boolean
48
+ | null
49
+ | ResponseAnyValue[]
50
+ | { [key: string]: ResponseAnyValue };
51
+
44
52
  export type TVLabsSessionRequestResponse = {
53
+ status: number;
54
+ path: string;
45
55
  request_id: string;
56
+ method: string;
57
+ req_body: ResponseAnyValue;
58
+ resp_body: ResponseAnyValue;
59
+ requested_at: string | null;
60
+ responded_at: string | null;
61
+ video_start_time: number | null;
62
+ video_end_time: number | null;
46
63
  };
47
64
 
48
65
  export type TVLabsSocketParams = TVLabsServiceInfo & {
@@ -71,3 +88,11 @@ export type TVLabsBuildMetadata = {
71
88
  size: number;
72
89
  sha256: string;
73
90
  };
91
+
92
+ export type TVLabsRequestMetadata = {
93
+ [key: string]: unknown;
94
+ };
95
+
96
+ export type TVLabsRequestMetadataResponse = {
97
+ [request_id: string]: TVLabsRequestMetadata;
98
+ };
@@ -0,0 +1,62 @@
1
+ # Integration Tests
2
+
3
+ These integration tests verify that the compiled ESM and CJS outputs work correctly when imported/required by consumers.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ test-integration/
9
+ ├── esm/
10
+ │ └── import.test.mjs # ESM module import tests
11
+ └── cjs/
12
+ └── require.test.mjs # CJS require tests (uses createRequire)
13
+ ```
14
+
15
+ ## Running Tests
16
+
17
+ ```bash
18
+ # Run all integration tests (builds first)
19
+ npm run test:integration
20
+
21
+ # Run only ESM integration tests
22
+ npm run test:integration:esm
23
+
24
+ # Run only CJS integration tests
25
+ npm run test:integration:cjs
26
+ ```
27
+
28
+ ## What These Tests Do
29
+
30
+ Unlike unit tests that test source code (`src/`), these integration tests:
31
+
32
+ 1. **Import/require the compiled outputs** (`esm/` and `cjs/`)
33
+ 2. **Verify the module structure** is correct (exports are available)
34
+ 3. **Test basic instantiation** to ensure the code runs in both module systems
35
+ 4. **Validate that the build process** produces functional output
36
+
37
+ ## How It Works
38
+
39
+ ### ESM Tests (`esm/import.test.mjs`)
40
+
41
+ - Uses standard ESM `import` to load the ESM build
42
+ - Imports directly from `../../esm/index.js`
43
+
44
+ ### CJS Tests (`cjs/require.test.mjs`)
45
+
46
+ - Written as an ESM file (`.mjs`) but uses `createRequire` to test CJS output
47
+ - Uses Node's `createRequire` API to dynamically require the CJS build
48
+ - This approach is necessary because Vitest itself is ESM-only
49
+
50
+ ## Prerequisites
51
+
52
+ Integration tests require the project to be built first:
53
+
54
+ ```bash
55
+ npm run build
56
+ ```
57
+
58
+ The `test:integration` script automatically runs the build step.
59
+
60
+ ## CI Integration
61
+
62
+ Integration tests run in CI as part of the Test job after unit tests complete.
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRequire } from 'module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { TVLabsService } = require('../../cjs/index.js');
6
+
7
+ const createService = (options = {}) => {
8
+ const serviceOptions = { apiKey: 'test-key', ...options };
9
+ const capabilities = {};
10
+ const config = { logLevel: 'silent' };
11
+ return new TVLabsService(serviceOptions, capabilities, config);
12
+ };
13
+
14
+ describe('CJS Integration Tests', () => {
15
+ it('should export TVLabsService', () => {
16
+ expect(typeof TVLabsService).toBe('function');
17
+ });
18
+
19
+ it('should be instantiable with valid options', () => {
20
+ const service = createService();
21
+ expect(service).toBeInstanceOf(TVLabsService);
22
+ });
23
+
24
+ it('should have onPrepare method', () => {
25
+ const service = createService();
26
+ expect(typeof service.onPrepare).toBe('function');
27
+ });
28
+
29
+ it('should have beforeSession method', () => {
30
+ const service = createService();
31
+ expect(typeof service.beforeSession).toBe('function');
32
+ });
33
+
34
+ it('should reject multi-remote capabilities in onPrepare', () => {
35
+ const service = createService({ continueOnError: true });
36
+ const multiRemoteConfig = {
37
+ capabilities: {
38
+ browserA: { browserName: 'chrome' },
39
+ browserB: { browserName: 'firefox' },
40
+ },
41
+ };
42
+
43
+ expect(() => service.onPrepare(multiRemoteConfig, {})).toThrow(
44
+ /multi-remote capabilities are not implemented/i,
45
+ );
46
+ });
47
+
48
+ it('should have lastRequestId method', () => {
49
+ const service = createService();
50
+ expect(typeof service.lastRequestId).toBe('function');
51
+ });
52
+
53
+ it('should have requestMetadata method', () => {
54
+ const service = createService();
55
+ expect(typeof service.requestMetadata).toBe('function');
56
+ });
57
+
58
+ describe('Authorization header injection', () => {
59
+ it('should inject Authorization header when not present', () => {
60
+ const config = {};
61
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
62
+
63
+ expect(config.headers).toBeDefined();
64
+ expect(config.headers.Authorization).toBe('Bearer test-api-key');
65
+ });
66
+
67
+ it('should not override existing Authorization header', () => {
68
+ const config = {
69
+ headers: {
70
+ Authorization: 'Bearer existing-token',
71
+ },
72
+ };
73
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
74
+
75
+ expect(config.headers.Authorization).toBe('Bearer existing-token');
76
+ });
77
+
78
+ it('should preserve other headers when injecting Authorization', () => {
79
+ const config = {
80
+ headers: {
81
+ 'X-Custom-Header': 'custom-value',
82
+ },
83
+ };
84
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
85
+
86
+ expect(config.headers['X-Custom-Header']).toBe('custom-value');
87
+ expect(config.headers.Authorization).toBe('Bearer test-api-key');
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { TVLabsService } from '../../esm/index.js';
3
+
4
+ const createService = (options = {}) => {
5
+ const serviceOptions = { apiKey: 'test-key', ...options };
6
+ const capabilities = {};
7
+ const config = { logLevel: 'silent' };
8
+ return new TVLabsService(serviceOptions, capabilities, config);
9
+ };
10
+
11
+ describe('ESM Integration Tests', () => {
12
+ it('should export TVLabsService', () => {
13
+ expect(typeof TVLabsService).toBe('function');
14
+ });
15
+
16
+ it('should be instantiable with valid options', () => {
17
+ const service = createService();
18
+ expect(service).toBeInstanceOf(TVLabsService);
19
+ });
20
+
21
+ it('should have onPrepare method', () => {
22
+ const service = createService();
23
+ expect(typeof service.onPrepare).toBe('function');
24
+ });
25
+
26
+ it('should have beforeSession method', () => {
27
+ const service = createService();
28
+ expect(typeof service.beforeSession).toBe('function');
29
+ });
30
+
31
+ it('should reject multi-remote capabilities in onPrepare', () => {
32
+ const service = createService({ continueOnError: true });
33
+ const multiRemoteConfig = {
34
+ capabilities: {
35
+ browserA: { browserName: 'chrome' },
36
+ browserB: { browserName: 'firefox' },
37
+ },
38
+ };
39
+
40
+ expect(() => service.onPrepare(multiRemoteConfig, {})).toThrow(
41
+ /multi-remote capabilities are not implemented/i,
42
+ );
43
+ });
44
+
45
+ it('should have lastRequestId method', () => {
46
+ const service = createService();
47
+ expect(typeof service.lastRequestId).toBe('function');
48
+ });
49
+
50
+ it('should have requestMetadata method', () => {
51
+ const service = createService();
52
+ expect(typeof service.requestMetadata).toBe('function');
53
+ });
54
+
55
+ describe('Authorization header injection', () => {
56
+ it('should inject Authorization header when not present', () => {
57
+ const config = {};
58
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
59
+
60
+ expect(config.headers).toBeDefined();
61
+ expect(config.headers.Authorization).toBe('Bearer test-api-key');
62
+ });
63
+
64
+ it('should not override existing Authorization header', () => {
65
+ const config = {
66
+ headers: {
67
+ Authorization: 'Bearer existing-token',
68
+ },
69
+ };
70
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
71
+
72
+ expect(config.headers.Authorization).toBe('Bearer existing-token');
73
+ });
74
+
75
+ it('should preserve other headers when injecting Authorization', () => {
76
+ const config = {
77
+ headers: {
78
+ 'X-Custom-Header': 'custom-value',
79
+ },
80
+ };
81
+ new TVLabsService({ apiKey: 'test-api-key' }, {}, config);
82
+
83
+ expect(config.headers['X-Custom-Header']).toBe('custom-value');
84
+ expect(config.headers.Authorization).toBe('Bearer test-api-key');
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['test-integration/cjs/**/*.test.mjs'],
8
+ coverage: {
9
+ enabled: false,
10
+ },
11
+ },
12
+ });
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['test-integration/esm/**/*.test.mjs'],
8
+ coverage: {
9
+ enabled: false,
10
+ },
11
+ },
12
+ });