Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socket.io client support (can also be used for other front-end clients on web) #534

Open
angelo-hub opened this issue Sep 22, 2023 · 0 comments
Labels
enhancement New feature or request

Comments

@angelo-hub
Copy link

angelo-hub commented Sep 22, 2023

Is your feature request related to a problem? Please describe.
As a user I would like to track socket events similar to how XHR requests are tracked, in our case, we use the feathers-client and this acts as almost a 2 way REST-API

Describe the solution you'd like

Ideally integrating a class that returns a modified version of the Socket.io client that tracks emit events, responses and responding .on events

Was able to accomplish this ourselves by reworking the XHR requestProxy code, and reviewing openTelemetry's Socket.io instrumentation

Will drop the example code below:

import { Socket } from 'socket.io-client';
import { BASE_URL } from '@env';

import { rum } from '#/global/utils/datadog';
import { Timer } from '#/global/utils/datadog/Timer';

export type FeathersEmitEvents =
  | 'patch'
  | 'create'
  | 'update'
  | 'remove'
  | 'get'
  | 'find';

export interface RumResourceContext {
  context: { eventType: 'on' | 'emit'; isSocketIO: boolean; params?: string };
  method: string;
  timer: Timer;
  url: string;
}

export interface SocketIOInstrumentationConfig {
  reportEmitEvents?: boolean;
  reportOnEvents?: boolean;
  reservedEvents?: string[];
}

function byteLength(str: string): number {
  // This is a weird trick, but it works.
  // Output is the same as TextEncoder.encode(...).length
  return unescape(encodeURI(str)).length;
}

const methodToRestKeyEmit = (method: FeathersEmitEvents | string) => {
  switch (method) {
    case 'patch':
    case 'update': {
      return 'PUT';
    }
    case 'create': {
      return 'POST';
    }
    case 'remove': {
      return 'DELETE';
    }
    case 'get':
    case 'find':
    default: {
      return 'GET';
    }
  }
};

const methodToRestKeyOn = (method: string) => {
  switch (method) {
    case 'patched':
    case 'updated': {
      return 'PUT';
    }
    case 'created': {
      return 'POST';
    }
    case 'removed': {
      return 'DELETE';
    }
    case 'get':
    case 'find':
    default: {
      return 'GET';
    }
  }
};

/*
 * DataDog metric tracking
 * Based on @external https://github.com/DataDog/dd-sdk-reactnative/blob/develop/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts
 */
export const watchSocket = (
  socket: Socket,
  config: SocketIOInstrumentationConfig = {},
) => {
  const {
    reportOnEvents = true,
    reportEmitEvents = true,
    reservedEvents = [],
  } = config;

  if (reportEmitEvents) {
    const originalEmit = socket.emit;

    socket.emit = function (...args: any[]) {
      // ignore reservedEvents
      if (reservedEvents.includes(args[1])) {
        // @ts-expect-error args is valid
        return originalEmit.apply(socket, args);
      }
      const context: RumResourceContext = {
        context: { eventType: 'emit', isSocketIO: true, params: '' },
        method: methodToRestKeyEmit(args[0]),
        timer: new Timer(),
        url: `${BASE_URL}/${args[1]}`,
      };

      // captures any extra data and passes it into the context of the payload
      if (typeof args[2] === 'string') {
        context.context.params = JSON.stringify({ id: args[2] });
      } else if (typeof args[2] === 'object') {
        context.context.params = JSON.stringify(args[2]);
      }

      context.timer.start();
      let statusCode = 200;
      let responseSize = -1;
      const key = `${context.timer.startTime}/${context.method}`;
      rum.startResource(
        key,
        context.method,
        context.url,
        context.context,
        context.timer.startTime,
      );
      const originalCallback = args[args.length - 1];
      const callback = function (...callbackArgs: any[]) {
        context.timer.stop();

        // since there can be multiple response arguments, calculate the response size via a reduce
        responseSize = callbackArgs.reduce(
          (acc, arg) => acc + byteLength(JSON.stringify(arg)),
          0,
        );

        // error is passed in as first value
        if (callbackArgs[0]) {
          statusCode = callbackArgs[0].code;
        }

        rum.stopResource(
          key,
          statusCode,
          'xhr',
          responseSize,
          context.context,
          context.timer.stopTime,
        );

        return originalCallback && typeof originalCallback === 'function'
          ? (originalCallback(...callbackArgs) as void)
          : undefined;
      };

      if (originalCallback && typeof originalCallback === 'function') {
        args[args.length - 1] = callback;
      } else {
        args.push(callback);
      }
      // @ts-expect-error args is valid
      return originalEmit.apply(socket, args);
    };
  }

  if (reportOnEvents) {
    socket.onAny((event: string, data: any) => {
      const [service, method] = event.split(' ');
      const startTime = Date.now();
      const methodKey = methodToRestKeyOn(method);

      const key = `${startTime}/${service}/${methodKey}`;
      const url = `${BASE_URL}/${service}`;
      const context: RumResourceContext['context'] = {
        eventType: 'on',
        isSocketIO: true,
      };

      rum.startResource(key, methodKey, url, context, startTime);
      rum.stopResource(
        key,
        200,
        'xhr',
        byteLength(JSON.stringify(data)),
        context,
        Date.now(),
      );
    });
  }
  return socket;
};

Describe alternatives you've considered

Was able to get this working but it would be great to have this as a first class citizen/RUM feature

Additional context

@angelo-hub angelo-hub added the enhancement New feature or request label Sep 22, 2023
@angelo-hub angelo-hub changed the title Socket.io client support (can also be used for other front-end clients on web Socket.io client support (can also be used for other front-end clients on web) Sep 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant