🖥️

NewRelic Scripting Monitorsの開発をローカルでデバッグしながら行う方法

2023/09/08に公開

はじめに

NewRelicにはScripting monitorsという、実際のブラウザを使ってエンドユーザーがアプリケーションを正常に利用できているかを確認する機能があります。

これはSelenium WebDriver 4.1 JSをによって実装されています。実行環境では$selenium, $webDriverというグローバル変数にSeleniumのインポートとWebDriverが生成されており、これを使ってブラウザを簡単に操作できます。

$webDriver.get("https://mywebsite.com");
$webDriver.get("https://my-website.com")
    .then(() => ($webDriver.findElement($selenium.By.linkText("Configuration Panel"))))
    .then((element) => (element.click()));

ですが、ローカルでTypeScriptを使って開発する場合はグローバル変数にこれらは定義されていないので、そのままでは実行できず、型も保管されません。
これだと長いスクリプトを継続的にメンテナンスしていくのが難しいと考え、ローカルでTypeScriptで開発しながらデバッグできる環境を作ったのでその方法を紹介します。

環境構築

ディレクトリの作成と必要なモジュールのインストール

まずはディレクトリを作成し、必要なモジュールをインストールします。

WORKDIR=nr-scripting-monitors
mkdir ${WORKDIR}
cd ${WORKDIR}

npm init --yes
npm i -D vitest typescript selenium-webdriver @types/selenium-webdriver esbuild
npx tsc --init

型定義の作成

グローバル変数の型定義を作成します。types/global.d.tsを作成し、以下のように記述します。
クレデンシャル情報が必要な場合は$secureも定義します。

import * as selenium from "selenium-webdriver";
import type { WebDriver } from "selenium-webdriver";

declare global {
  var $selenium: typeof selenium;
  var $webDriver: WebDriver;

  // var $secure: {
  //   YOUR_SECRET: string;
  // };
}

この型定義を読み込むようにtsconfig.jsonを編集します。合わせて"strict": falseを設定する必要があるので注意してください。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "types": ["types/*.d.ts"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": false,
    "skipLibCheck": true
  }
}

Vitest設定の作成

テストにはVitestを使いました。vitest.config.tsを作成し、以下のように記述します。
VitestはデフォルトでWatchモードで動作します。これだと保存する度にブラウザが立ち上がりテストが実行され不便だったので無効化しました。

/// <reference types="vitest/config" />
import { defineConfig } from "vite";

export default defineConfig({
  test: {
    watch: false,
  },
});

Selenium用のユーティリティの作成

Scripting monitorsの以前のバージョンではwaitForAndFindElementというHTML Elementが配置・可視化されるのを待つ便利なユーティリティが提供されていました。ドキュメントには以前のバージョンの情報も多く残っており同じ動作を再現する関数を作成します。

参考元: https://docs.newrelic.com/docs/synthetics/synthetic-monitoring/scripting-monitors/synthetic-scripted-browser-reference-monitor-versions-chrome-100/#structure

utils/selenium.ts

import { Locator, WebElement } from "selenium-webdriver";

export const waitForAndFindElement = async (
  locator: Locator,
  { timeout }: { timeout?: number } = { timeout: 3000 }
): Promise<WebElement> => {
  const element = await $webDriver.wait(
    $selenium.until.elementLocated(locator),
    timeout
  );
  await $webDriver.wait($selenium.until.elementIsVisible(element), timeout);
  return $webDriver.findElement(locator);
};

export const waitUrlContains = async (
  url: string,
  { timeout }: { timeout?: number } = { timeout: 3000 }
): Promise<void> => {
  await $webDriver.wait($selenium.until.urlContains(url), timeout);
  return;
};

テストの作成

型定義は作成しましたが、それだけでは$selenium, $webDriverを使ってローカルでデバッグすることは出来ません。vitestのvi.stubGlobal()を使ってグローバル変数を書き換えることで実行可能にします。
ローカルでのデバッグなのでテストのタイムアウト時間を長めに設定しておきます。実際の環境では3分以上かかるスクリプトは実行できないので注意してください。

my-scenario.test.ts

import { Browser, Builder } from "selenium-webdriver";
import { Options } from "selenium-webdriver/chrome";
import { describe, it, vi } from "vitest";
import { myScenario } from "./my-scenario";

describe(
  "サンプル",
  async () => {
    // const secure = {
    //   YOUR_KEY: process.env.YOUR_KEY,
    // };
    // vi.stubGlobal("$secure", secure);
    const webDriver = await new Builder()
      .forBrowser(Browser.CHROME)
      .build();
    vi.stubGlobal("$webDriver", webDriver);
    const selenium = require("selenium-webdriver");
    vi.stubGlobal("$selenium", selenium);

    await myScenario();
  },
  { timeout: 3000000 }
);

package.jsonを設定してテストにvitestを指定します。

"scripts": {
    "test": "vitest"
  },

スクリプトの作成

これでこんな風にスクリプトを作成できます。

my-scenario.ts

import { waitForAndFindElement } from "./utils/selenium";

export const myScenario = async () => {
  $webDriver.get("https://www.google.com/");
  await waitForAndFindElement(
    $selenium.By.xpath(
      "/html/body/div[1]/div[3]/form/div[1]/div[1]/div[1]/div/div[2]/textarea"
    )
  ).then((e) => e.sendKeys("newrelic"));
  await waitForAndFindElement($selenium.By.xpath("//*[@type='submit']")).then(
    (e) => e.click()
  );
};

npm testを実行することでChromeが起動しデバッグができます。(何も検証していないのでテスト自体はエラーになります。)

ビルドの設定

my-scenario.tsだけではNewRelic上で実行出来ないのでハンドラー関数を作成します。

handlers/my-scenario.ts

import { myScenario } from "../my-scenario";

(async () => await myScenario())();

Scripting monitorsはTop Level awaitをサポートしていますが、トランスコンパイルの設定などが面倒だったので即時実行関数でラップする事で対応しました。

package.jsonを編集し、ビルドの設定を追加します。

"scripts": {
    "build": "esbuild handlers/my-scenario.ts --bundle --outfile=dist/my-scenario.js",
    "test": "vitest"
  },

ちなみにですが、Scripting monitorsは実行環境にgotを内包しているので、これを使うことで外部APIと通信しながらモニタリングが行います。
この際にesbuildのオプションで--external:gotとオプションを指定することでバンドルから除外する事が出来ます。

npm run buildを実行し、distディレクトリに生成されたコードをScripting monitorsに登録すれば完了です。

あとがき

やり方を確立してしまえば簡単なのですが、ESMが使えるか試したりdev container上で実行してみようとしたり試行錯誤していたら完成まで2日使ってしまいました。dev container上で実行するとchromiumを使う必要やheadlessになってしまいデバッグとしては使いづらくなるので、この作業だけはホストOS上で直接行うように妥協しました。

Scripting monitorsはまだ使い始めたばかりですが、実際にブラウザを通してエンドユーザーの挙動が出来るかを継続的に監視ができ、実行中の通信の時間や容量もわかり便利そうでした。

以上です!

東急URBAN HACKS

Discussion