NewRelic Scripting Monitorsの開発をローカルでデバッグしながら行う方法
はじめに
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が配置・可視化されるのを待つ便利なユーティリティが提供されていました。ドキュメントには以前のバージョンの情報も多く残っており同じ動作を再現する関数を作成します。
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はまだ使い始めたばかりですが、実際にブラウザを通してエンドユーザーの挙動が出来るかを継続的に監視ができ、実行中の通信の時間や容量もわかり便利そうでした。
以上です!
Discussion