[laravel8.12.3] Seederでテストデータを2000件追加してMySQLの実行速度(ユーザCPU時間)を測定する

1. DatabaseSeeder
./database/seeds/DatabaseSeeder.php

 public function run()
    {
        $this->call(Orders1TableSeeder::class);
    }

2. make seeder
$ php artisan make:seeder Orders1TableSeeder

use Illuminate\Support\Facades\DB;

public function run()
    {
        DB::table('orders1')->insert([
            'user_id'=>rand(1,10),
            'supplier_num'=>rand(1,10),
            'supplier_id1'=>rand(1,10),
            'supplier_id2'=>rand(1,10),
            'supplier_id3'=>rand(1,10),
            'supplier_subtotal1'=>rand(100,10000),
            'supplier_subtotal2'=>rand(100,10000),
            'supplier_subtotal3'=>rand(100,10000),
            'total'=>rand(100,10000),
            'product_num'=>rand(1,10),
            'product_id1'=>rand(1,10),
            'product_id2'=>rand(1,10),
            'product_id3'=>rand(1,10),
            'product_id4'=>rand(1,10),
            'product_id5'=>rand(1,10),
            'qty1'=>rand(1,10),
            'qty2'=>rand(1,10),
            'qty3'=>rand(1,10),
            'qty4'=>rand(1,10),
            'qty5'=>rand(1,10),
            'subtotal1'=>rand(100,10000),
            'subtotal2'=>rand(100,10000),
            'subtotal3'=>rand(100,10000),
            'subtotal4'=>rand(100,10000),
            'subtotal5'=>rand(100,10000),
            'detail'=>'',
        ]);
    }

3. db:seed
$ php artisan db:seed

mysql> select * from orders1;
+—-+———+————–+————–+————–+————–+——————–+——————–+——————–+———+————-+————-+————-+————-+————-+————-+——+——+——+——+——+———–+———–+———–+———–+———–+——–+————+————+
| id | user_id | supplier_num | supplier_id1 | supplier_id2 | supplier_id3 | supplier_subtotal1 | supplier_subtotal2 | supplier_subtotal3 | total | product_num | product_id1 | product_id2 | product_id3 | product_id4 | product_id5 | qty1 | qty2 | qty3 | qty4 | qty5 | subtotal1 | subtotal2 | subtotal3 | subtotal4 | subtotal5 | detail | created_at | updated_at |
+—-+———+————–+————–+————–+————–+——————–+——————–+——————–+———+————-+————-+————-+————-+————-+————-+——+——+——+——+——+———–+———–+———–+———–+———–+——–+————+————+
| 1 | 6 | 6 | 6 | 9 | 9 | 6713.00 | 7337.00 | 6092.00 | 9497.00 | 6 | 10 | 2 | 10 | 2 | 2 | 10 | 5 | 5 | 6 | 6 | 4003.00 | 7581.00 | 1475.00 | 2006.00 | 4347.00 | | NULL | NULL |
+—-+———+————–+————–+————–+————–+——————–+——————–+——————–+———+————-+————-+————-+————-+————-+————-+——+——+——+——+——+———–+———–+———–+———–+———–+——–+————+————+
1 row in set (0.00 sec)

mysql> truncate table orders1;

4. seederで2000件データを挿入する
for文で2000件入れる

    public function run()
    {
    	for($i=0;$i<=2000; $i++){
    		DB::table('orders1')->insert([
	            // 省略
	        ]);
    	}
        
    }

$ php artisan db:seed

5. mysql文
mysql.sql

select * from orders1 where supplier_num = 1;

$ time (cat mysql.sql | mysql -u root -p test > /dev/null)
Enter password:

real 0m3.099s // プログラムの呼び出しから終了までにかかった実時間
user 0m0.009s // プログラム自体の処理時間(秒)(ユーザCPU時間)
sys 0m0.012s // プログラムを処理するために、OSが処理をした時間

OK
これはカラム数29だが、カラム数を50, 100と増やした場合に、実行時間がどれ位変化するかテストする。
うむ、そこそこテストっぽいことやってる。

[laravel8.12.3] テーブル追加の手順

まず適当にテーブルを作ります。
$ php artisan make:migration create_orders1_table

続いて適当にカラムを作ります。
-> カラム数29
2020_11_26_122330_create_orders1_table.php

public function up()
    {
        Schema::create('orders1', function (Blueprint $table) {
            $table->id();
            $table->integer('user_id');
            $table->integer('supplier_num');
            $table->integer('supplier_id1')->nullable();
            $table->integer('supplier_id2')->nullable();
            $table->integer('supplier_id3')->nullable();
            $table->decimal('supplier_subtotal1', 10, 2)->nullable();
            $table->decimal('supplier_subtotal2', 10, 2)->nullable();
            $table->decimal('supplier_subtotal3', 10, 2)->nullable();
            $table->decimal('total', 10, 2);
            $table->integer('product_num');
            $table->integer('product_id1')->nullable();
            $table->integer('product_id2')->nullable();
            $table->integer('product_id3')->nullable();
            $table->integer('product_id4')->nullable();
            $table->integer('product_id5')->nullable();
            $table->integer('qty1')->nullable();
            $table->integer('qty2')->nullable();
            $table->integer('qty3')->nullable();
            $table->integer('qty4')->nullable();
            $table->integer('qty5')->nullable();
            $table->decimal('subtotal1', 10, 2)->nullable();
            $table->decimal('subtotal2', 10, 2)->nullable();
            $table->decimal('subtotal3', 10, 2)->nullable();
            $table->decimal('subtotal4', 10, 2)->nullable();
            $table->decimal('subtotal5', 10, 2)->nullable();
            $table->text('detail')->nullable();
            $table->timestamps();
        });
    }

適当にmigrateします。
$ php artisan migrate
Migrating: 2020_11_26_122330_create_orders1_table
Migrated: 2020_11_26_122330_create_orders1_table (53.41ms)

mysql> describe orders1;
+——————–+—————–+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+——————–+—————–+——+—–+———+—————-+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| user_id | int | NO | | NULL | |
| supplier_num | int | NO | | NULL | |
| supplier_id1 | int | YES | | NULL | |
| supplier_id2 | int | YES | | NULL | |
| supplier_id3 | int | YES | | NULL | |
| supplier_subtotal1 | decimal(10,2) | YES | | NULL | |
| supplier_subtotal2 | decimal(10,2) | YES | | NULL | |
| supplier_subtotal3 | decimal(10,2) | YES | | NULL | |
| total | decimal(10,2) | NO | | NULL | |
| product_num | int | NO | | NULL | |
| product_id1 | int | YES | | NULL | |
| product_id2 | int | YES | | NULL | |
| product_id3 | int | YES | | NULL | |
| product_id4 | int | YES | | NULL | |
| product_id5 | int | YES | | NULL | |
| qty1 | int | YES | | NULL | |
| qty2 | int | YES | | NULL | |
| qty3 | int | YES | | NULL | |
| qty4 | int | YES | | NULL | |
| qty5 | int | YES | | NULL | |
| subtotal1 | decimal(10,2) | YES | | NULL | |
| subtotal2 | decimal(10,2) | YES | | NULL | |
| subtotal3 | decimal(10,2) | YES | | NULL | |
| subtotal4 | decimal(10,2) | YES | | NULL | |
| subtotal5 | decimal(10,2) | YES | | NULL | |
| detail | text | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+——————–+—————–+——+—–+———+—————-+
29 rows in set (0.00 sec)

続いて、このテーブルにSeederでテストデータを入れたい。

[mysql8.0.22]実行速度(ユーザCPU時間)の確認方法

mysql> select version();
+———–+
| version() |
+———–+
| 8.0.22 |
+———–+
1 row in set (0.00 sec)

mysql> use test;
mysql> select * from users;
mysql> SELECT BENCHMARK(100,(SELECT id FROM users m0 WHERE (m0.id = 1)));
+————————————————————+
| BENCHMARK(100,(SELECT id FROM users m0 WHERE (m0.id = 1))) |
+————————————————————+
| 0 |
+————————————————————+
1 row in set (0.00 sec)

ん? 0秒ってこと?

$ touch mysql.sql

select * from users where id = 1;
$ mysql -u root -p "password" -D test <mysql.sql
$ cat mysql.sql | mysql -u root -p -vvv test | tail -n 3
Enter password: 

1 row in set (0.00 sec)

Bye

$ time (cat mysql.sql | mysql -u root -p test > /dev/null)
Enter password: 

real 0m2.544s // プログラムの呼び出しから終了までにかかった実時間(秒)
user 0m0.013s // プログラム自体の処理時間(秒)(ユーザCPU時間)
sys 0m0.007s // プログラムを処理するために、OSが処理をした時間(秒)(システム時間)

0.013s秒ってことか。

これをレコード4000件で、テーブルのカラム数10, 30, 100でテストしてみるか。
結構大変だな。

[CircleCI 2.0] Laravel8.15.0で使用する

# 前準備
1. テスト用の.envファイル作成
$ cp .env.example .env.testing

2. .env.testingのAPP_KEY作成
$ php artisan key:generate –env=testing

.env.testing

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=circle_test
DB_USERNAME=hogehoge
DB_PASSWORD=fugafuga

DB_DATABASEは、circle_testする。

3. テストコード作成
$ php artisan make:test UserRegisterTest

4. デフォルトのテストコードは削除
$ rm ./tests/Feature/ExampleTest.php
$ rm ./tests/Unit/ExampleTest.php

tests/Feature/UserRegisterTest.php
L RefreshDatabaseを使用すると、各テスト前後にマイグレーションとロールバックを実行

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class UserRegisterTest extends TestCase
{
    use RefreshDatabase;
    public function testExample()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

5. UnitTestのテスト
$ vendor/bin/phpunit –testdox
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.

User Register (Tests\Feature\UserRegister)
✔ Example

Time: 00:00.646, Memory: 26.00 MB

OK (1 test, 1 assertion)

6. ユーザ登録のテスト
$ php artisan route:list | grep register
| | GET|HEAD | register | register | Laravel\Fortify\Http\Controllers\RegisteredUserController@create | App\Http\Middleware\EncryptCookies |
| | POST | register | | Laravel\Fortify\Http\Controllers\RegisteredUserController@store | App\Http\Middleware\EncryptCookies |

tests/Feature/UserRegisterTest.php

class UserRegisterTest extends TestCase
{
    use RefreshDatabase;

    public function testUserRegister()
    {
        $email = 'hogehoge@gmail.com';
        $this->post(route('register'), [
            'name' => 'user1',
            'email' => $email,
            'password' => 'password',
            'password_confirmation' => 'password'
        ])
            ->assertStatus(302);

        $this->assertDatabaseHas('users', ['email' => $email]);
    }
}

$ vendor/bin/phpunit –testdox
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.

User Register (Tests\Feature\UserRegister)
✔ User register

Time: 00:00.463, Memory: 28.00 MB

OK (1 test, 2 assertions)

※functionはtestで始まる必要があり、userRegisterだと、No tests found in class “Tests\Feature\UserRegisterTest”となる。

これをCircleCIで行う

# CircleCIでのテスト
1. config/database.php
L circle_testingを追記する

        'circle_test' => [
            'driver' => 'mysql',
            'host' => '127.0.0.1',
            'port' => '3306',
            'database' => 'circle_test',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

2. GithubでPrivateのrepositoryを作成し、projectをgit pushします。
$ git init
$ git add .
$ git commit -m “first commit”
$ git remote add origin https://github.com/hoge/fuga.git

3. CircleCIにログインしてSetupProject

Add Configボタン押下

This package requires php ^7.3|^8.0 but your PHP version (7.1.33) does not satisfy that requirement.

config.ymlのdocker imageがphp:7.1になってるからエラー。ここを7.3以上に修正する必要がある。
– dockerはphp:7.4.7を、MySQLは8系を使う
https://circleci.com/docs/ja/2.0/circleci-images/
mysql8系から認証方法が変わったので、command: [–default-authentication-plugin=mysql_native_password]を追記

version: 2
jobs:
  build:
    docker:
      - image: circleci/php:7.4.7-apache-node-browsers
      - image: circleci/mysql:8.0
        command: [--default-authentication-plugin=mysql_native_password]
 
    environment:
      - APP_DEBUG: true
      - APP_ENV: testing
      - APP_KEY: base64:hogehogefugafuga
      - DB_CONNECTION: circle_test
      - MYSQL_ALLOW_EMPTY_PASSWORD: true
 
    working_directory: ~/repo
 
    steps:
      - checkout
 
      - run: sudo docker-php-ext-install pdo_mysql
 
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "composer.json" }}
          - v1-dependencies-
 
      - run: composer install -n --prefer-dist
 
      - save_cache:
          paths:
            - ./vendor
          key: v1-dependencies-{{ checksum "composer.json" }}
 
      - run: php artisan migrate
      - run: php artisan db:seed
 
      - run: php ./vendor/bin/phpunit

$ git add .
$ git config –global core.autoCRLF false
$ git commit -m “circleci”
$ git remote add origin https://github.com/hoge/hoge.git
$ git push -u origin master

復習にちょっと時間がかかり過ぎてしまったが、まぁOKでしょう。というか、Djangoで開発するときも、ちゃんとCircleCI使えばよかった。
これを実装していきます。

[Google Analytics API] Chart.jsでユーザ範囲指定のPVを表示したい

まずChart.jsの復習から
公式ドキュメントを元にテスト表示します。

<body>
	<h1>折れ線グラフ</h1>
	<div >
		<canvas id="myLineChart" width="400" height="200"></canvas>
	</div>
	<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
	<script>
		var ctx = document.getElementById('myLineChart').getContext('2d');
		var chart = new Chart(ctx, {
			type: 'line',

			data: {
				labels: ['january', 'February', 'March', 'April', 'May', 'June', 'July'],
				datasets: [{
					label: 'ページビュー',
					backgroundColor: 'rgb(0, 0, 0, 0)',
					borderColor: 'rgb(81, 203, 206)',
					data: [0, 10, 5, 2, 20, 30, 45],
				}]
			},

			options: {
				responsive: false
			}
		});
	</script>
</body>
</html>

PHP側から日付とpageviewsの配列をjasonでjs側に渡す

$result = $data -> rows;
foreach($result as $key => $value){
  $date[] = $value[0];
  $pageviews[] = $value[1];
}
$date = json_encode($date);
$pageviews = json_encode($pageviews);

js側でJSON.parseで受け取る

data: {
        labels: JSON.parse('<?php echo $date; ?>'),
        datasets: [{
          label: 'ページビュー',
          lineTension: 0,
          backgroundColor: 'rgb(0, 0, 0, 0)',
          borderColor: 'rgb(81, 203, 206)',
          data: JSON.parse('<?php echo $pageviews; ?>'),
        }]
      },

日付をYYYYMMDDではなく、本家のanalyticsみたいにMM月DD日にしたい。
-> substrでMMとDDを切り抜く

foreach($result as $key => $value){
  $date[] = substr($value[0], 4, 2) . "月" . substr($value[0], 6) . "日";
  $pageviews[] = $value[1];
}

これをユーザ入力で表示させます。

if(isset($_GET['datepicker_s']) && $_GET['datepicker_s'] !== ""){
  $start_day = $_GET['datepicker_s'];

  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $end_day = $_GET['datepicker_e'];
  } else {
    $end_day = date("Y-m-d");
  }

} else {
  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $start_day = date("Y-m-d",strtotime("-1 week",  strtotime($_GET['datepicker_e'] . " 00:00:00")));
    $end_day = $_GET['datepicker_e'];
  } else {
    $start_day = date("Y-m-d",strtotime("-1 week"));
    $end_day = date("Y-m-d");
  }
}

chartjsのtypeがlineだと、datasetが1つの時、以下の様にバグになってしまう。

type=”bar”とする

うーん、悪くないけど、やっぱ棒グラフが良いな

[Google Analytics API] 日付別のPVを取得したい

dimensionをga:dateにして、metricsをga:pageviewsにする。
$data[“rows”][0][“date”][“pv”] みたいな感じで返ってくるので、一度、rowsを取得してから、foreachで回すとdateとpvを取得できる。

$start_day = date("Y-m-d",strtotime("-1 week"));
$end_day = date("Y-m-d");


$data = $analytics->data_ga->get(
  'ga:' . $profile,
  $start_day,
  $end_day,
  'ga:pageviews',
 array(
   "dimensions" => 'ga:date',
   "metrics" => 'ga:pageviews',
   "sort" => 'ga:date',
  )
);
$result = $data -> rows;
foreach($result as $key => $value){
 echo  '日付:' . $value[0]. ' PV:' . $value[1] . "<br>";
}

うん、これをchart.jsで表示したい。できれば、ユーザがformで日付(開始日、終了日)を入力して、それを表示。
PHPからJavascriptにjsonで渡して表示でOKだと思うんだが、Chart.jsの書き方忘れたから復習からだ。

[Google Analytics API] ユーザが範囲指定した値のデータを表示する

– ユーザが範囲指定した期間のPV、セッション、上位閲覧ページを表示したい。
– UIとしては以下のように、開始日と終了日のDatepickerがある。

### form
まず、formはgetメソッドで作成する

<form action="" method="get">
      <div class="search">
      <div class="form-group form-inline ">
        <label for="" class="">注文日</label>
        <input type="text" class="form-control" name="datepicker_s" placeholder="">
        <label for="" class=""> ~ </label>
        <input type="text" class="form-control" name="datepicker_e" placeholder="">
        <button type="submit" class="btn">検索</button>
      </div>
      </div>
    </form>

### js
– 開始日は終了日より以前、終了日は開始日より以降 をjsのon changeで制御する
– 開始日、終了日共にmaxDateは今日までとする

<script
  src="https://code.jquery.com/jquery-3.5.1.js"
  integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="
  crossorigin="anonymous"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
  	<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/i18n/jquery.ui.datepicker-ja.min.js"></script>
  	<script>
    $(function(){
      var format = 'yy-mm-dd';

      var start = $("[name=datepicker_s]").datepicker({
        dateFormat: 'yy-mm-dd',
        maxDate: '0'
      }).on("change", function(){
        end.datepicker("option", "minDate", getDate(this));
      });

      var end = $("[name=datepicker_e]").datepicker({
        dateFormat: 'yy-mm-dd',
        maxDate: '0',
      }).on("change", function(){
        start.datepicker("option", "maxDate", getDate(this));
      });

      function getDate(element){
        var date;
        try {
          date = $.datepicker.parseDate(format, element.value);
        } catch(error){
          date = null;
        }
        return date;
      }
    });
  </script>

### php
– (1)開始日、終了日入力なし、(2)開始日のみ入力、(3)終了日のみ入力、(4)開始日、終了日入力あり の4パターンを書いてやれば良い
– 「終了日のみ入力」の際に、開始日をいつに設定するかは決めの問題。便宜上、下では終了日の一週間前にしている

if(isset($_GET['datepicker_s']) && $_GET['datepicker_s'] !== ""){
  $start_day = $_GET['datepicker_s'];

  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $end_day = $_GET['datepicker_e'];
  } else {
    $end_day = date("Y-m-d");
  }

} else {
  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $start_day = date("Y-m-d",strtotime("-1 week",  strtotime($_GET['datepicker_e'] . " 00:00:00")));
    $end_day = $_GET['datepicker_e'];
  } else {
    $start_day = date("Y-m-d",strtotime("-1 week"));
    $end_day = date("Y-m-d");
  }
}

$data = $analytics->data_ga->get(
  'ga:' . $profile,
  $start_day,
  $end_day,
  'ga:pageviews',
);
$pv = $data -> rows[0][0];

この分岐処理は、ハマると時間がかかるんだよなー

[Google Analytics API] タイトルとURLをPV順にソートして表示

この辺を押さえておけば大丈夫そう
訪問数: ga:sessions
合計PV: ga:pageviews
平均閲覧ページ数: ga:pageviewsPerSession
平均滞在時間: ga:avgSessionDuration
直帰率: ga:bounceRate

タイトル: ga:pageTitle
パス: ga:pagePath

require_once 'google-api-php-client/src/Google/autoload.php';
$service_account_email = '*';
$key = file_get_contents('analytics-*-*.p12');
$profile = '*';

$client = new Google_Client();
$analytics = new Google_Service_Analytics($client);

$cred = new Google_Auth_AssertionCredentials(
	$service_account_email,
	array(Google_Service_Analytics::ANALYTICS_READONLY),
	$key
);

$client->setAssertionCredentials($cred);
if($client->getAuth()->isAccessTokenExpired()){
	$client->getAuth()->refreshTokenWithAssertion($cred);
}

$pv = $analytics->data_ga->get(
	'ga:' . $profile,
	'7daysAgo',
	'yesterday',
	'ga:pageviews',
);
echo $pv -> rows[0][0] . "\n";


$result = $analytics->data_ga->get(
	'ga:' . $profile,
	'7daysAgo',
	'yesterday',
	'ga:pageviews',
	array(
		// "dimensions" => 'ga:pageTitle',
		"dimensions" => 'ga:pagePath',
		"sort" => '-ga:pageviews',
		"max-results" => '10',
	)
);

$data = $result->rows;

foreach($data as $key => $row){
	echo  ($key + 1) . ':' . $row[0]. ' ' . $row[1] . "\n";
}

ちょっと処理に時間がかかるので、テーブル作ってトップページの値はCronでデータベースに格納しておくか。
あとは日付指定のところをDatepicker & Postで作りたい。
todayは0daysAgo, yesterdayは’1daysAgo’でもいけるみたいだけど、[0-9]+(daysAgo)は9日前までしかできないから、YYYY-MM-DDの方が良さそうですね。
[0-9]{4}-[0-9]{2}-[0-9]{2}|today|yesterday|[0-9]+(daysAgo)

https://developers.google.com/analytics/devguides/reporting/core/v3/reference?hl=ja

[webpack 5.6.0]webpack-dev-server^3.11.0でMODULE_NOT_FOUND

$ npm run start

> front@1.0.0 start /home/vagrant/dev/front
> webpack-dev-server

internal/modules/cjs/loader.js:883
throw err;
^

Error: Cannot find module ‘webpack-cli/bin/config-yargs’
Require stack:
– /home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)
at Function.Module._load (internal/modules/cjs/loader.js:725:27)
at Module.require (internal/modules/cjs/loader.js:952:19)
at require (internal/modules/cjs/helpers.js:88:18)
at Object. (/home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js:65:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) {
code: ‘MODULE_NOT_FOUND’,
requireStack: [
‘/home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js’
]
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! front@1.0.0 start: `webpack-dev-server`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the front@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /home/vagrant/.npm/_logs/2020-11-20T05_45_43_741Z-debug.log

こちらのissueを参考に修正する
https://github.com/webpack/webpack-dev-server/issues/2759

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve --mode development --env development"
  },

$ npm run start

これで上手くいきます。