Du souhait d'achat d'un vélo ...

Posté le 27. September 2021 dans Programmation

Que penseriez-vous si je vous racontais un peu mes vacances ? Attendez ... attendez ... ne partez pas ... l'histoire est intéressante, et surtout nous allons parler informatique.

Début Août j'ai décidé de m'acheter un nouveau vélo (un VTC à assistance électrique). Le choix du vélo importe peu, mais du fait d'une pénurie de matière première et d'une forte demande en vélo depuis le début de la crise de mes sanitaire, tous les vélos sont en rupture de stock.

J'ai fait le choix personnel de me rendre dans une enseigne connue dont le nom est aussi une discipline de l'athlétisme pour acheter ce VTC.1

Et là c'est le drame.

Si vous regardez les différents vélos de la marque (et en fonction de la taille du cadre qui vous correspond) vous tombez sur le message: En rupture de stock. (Bon. À aujourd'hui, nous avons un peu plus de stock sur le site)

Arf. Moi qui quand j'ai décidé quelque chose, je deviens impatient ...

Alors il est vrai que j'aurais pu aller voir sur un autre site, aller dans une autre boutique pour choisir un autre modèle, mais ce n'est pas ce que j'ai fait.

L'attente

J'ai donc commencé par attendre, en allant régulièrement sur le site ou en rafraîchissant l'onglet d'un navigateur. C'est long, la page est relativement lourde pour un site WEB: plus de 2.2M de fichier à transférer pour un total de 175 requêtes et ce malgrès la présence d'un adblock.

De plus je dois sélectionner manuellement la taille dans une select box pour voir : Rupture de stock sur cette taille. Là je clique sur le bouton Vérifier le stock en magasin, ... je rentre mon code postal, ... et je regarde sur l'ensemble des magasins de ma région...

Bref, je prépare mes valises, et je pars me mettre au vert. Je fais un peu de marche avec les enfants (C'est les vacances quoi). Et je ne peux donc pas m'amuser à rafraîchir en continu (même si je le fais depuis mon téléphone portable).

Bien sûr j'ai cliqué sur le bouton M'avertir lorsque le produit est à nouveau disponible mais je me demande si ce bouton fonctionne, je n'ai à aujourd'hui toujours pas recu de mail :D.

Je décide donc de passer à l'étape suivante.

La 1er version

Je me pose donc et me demande comment je peux mettre mes compétences à contribution pour m'aider moi-même à m'acheter pour moi mon propre vélo. Je décide donc de mon propre chef de me développer une application sur mon compte AWS que je viens de me créer. Ce programme devra me permettre de faire plusieurs choses:

  1. En un coup d'oeil visualiser le stock pour le vélo sur Internet et dans les différents magasins
  2. M'avertir si le stock change
  3. Et le plus important, ne pas me prendre la tête et développer l'application dans le moins de temps possible

La 1er version que je développe a pour but de visualiser le stock.

Selecteur JS

Le but de l'application est de récupérer l'état des stocks du site Internet. Si je peux le faire avec un navigateur internet sur le site, alors un programme peut le faire aussi. Je prends les outils dont j'ai l'habitude pour faire le travail: Node.JS et le framework Nest.JS - framework que j'apprécie pour sa simplicité d'utilisation.

Pour parser la page, j'utilise JSDom:

const dom = await JSDOM.fromURL(VTC_540E_URL, options);

console.log(dom.window.document.querySelector("#product-size-selection"));

Première erreur quand je lance le programme:

(node:15246) UnhandledPromiseRejectionWarning: Error: Parse Error: Header overflow
    at TLSSocket.socketOnData (_http_client.js:476:22)
    at TLSSocket.emit (events.js:311:20)
    at addChunk (_stream_readable.js:294:12)
    at readableAddChunk (_stream_readable.js:275:11)
    at TLSSocket.Readable.push (_stream_readable.js:209:10)
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:186:23)
(node:15246) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:15246) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Pas de chance, les headers http de la grande enseigne contiennent tellement de blabla que nodejs limite et bloque. C'est l'effet CSP (Content-Security-Policy). Les CSP sont utilisés pour bloquer les requêtes faites par le navigateur sur une page si elle ne sont pas autorisées.

Par exemple, si un utisateur arrive à profiter d'une faille et arrive dans un avis client à mettre une iframe, une image, ... vers un site dont il possède le contrôle. La CSP va dire au navigateur que ce site n'est pas autorisé. Et le hack de l'utilisateur ne fonctionnera pas totalement.

Mais quand un site commence à avoir beaucoup de "partenaire" (google, vimeo, cloudfront, facebook, gstatic.com, et tous les trucs bloqués par adblock ...), la liste prend de la place.

Je relance node avec l'option adéquate:

node --max-http-header-size=80000 app.js

HTMLSelectElement {}

Cela commence bien.

Maintenant nous allons devoir ouvrir le sélecteur de taille, choisir la taille L (qui me correspond) et cliquer dessus. Je pourrai alors récupérer l'info de stock.

Malheureusement beaucoup de choses sur le site sont en JavaScript, JSDom ne permet pas d'executer le JS de la page2 et donc je suis bloqué. Déjà, pour un truc que je voulais faire rapidement, mauvais choix de solution ;). Je commence à me dire que je vais devoir sortir la grosse artillerie : Puppeteer.

Selecteur JS

Puppeteer est une librairie javascript qui permet de contrôler le navigateur chrome en mode headless (sans fenêtre). Cela me permettrait donc de parser la page en simulant avec un navigateur chrome.

Mais attendez, s'il y a beaucoup de javascript, il y a peut-être des requêtes XHR ?

XHR

XHR, c'est pour XMLHttpRequest, ce sont des requêtes executées par le Javascript pour récupérer quelques informations.

Avec Chrome, on peut récupérer la liste des requêtes qui sont effectuées lors de l'affichage de la page mais également lors des différentes interactions.

Vue network de Chrome

On active alors le filtre Fetch/XHR qu nous permet de voir les requêtes qui ne sont pas du CSS, ni du HTML, ni du JS, mais uniquement des requêtes qui sont lancées par le code Javascript.

Bien sûr il faut éliminer les requêtes vers d'autres sites (comme abtasty, ...). On peut alors retrouver la requête qui permet de retourner le stock du site Internet:

GET /fr/ajax/nfs/stocks/online?skuIds=4216661,4216663,4216662,4216664

Et avec la réponse:

{
  "4216661": {
    "stockOnline": 0
  },
  "4216662": {
    "stockOnline": 0
  },
  "4216663": {
    "stockOnline": 0
  },
  "4216664": {
    "stockOnline": 18
  }
}

La requête nous retourne les différents stocks pour les différentes tailles de cadre (S, M, L, XL). C'est la taille L qui m'intéresse, mais comment savoir quel code produit correspond à quelle taille ?

Code source de la page

On peut afficher le code source de la page (clic droit sur la page HTML : Afficher la source). Dans le source HTML on retrouve le bout de code suivant :

<script id="__dkt" type="application/json">
  ...
</script>

Le JSON contient le contexte qui sera utilisé par le code Javascript pour la génération de la page et le côté interactif. C'est l'équivalent d'un appel XHR intégré au démarrage de la page. On peut y retrouver ce que l'on cherche :

{
  "skus": [
    {
      "skuId": "4216661",
      "size": "S",
      "grossWeight": "20.9",
      "price": 2199,
      "isNotAvailable": true,
      "availableInStores": []
    },
    {
      "skuId": "4216663",
      "size": "M",
      "grossWeight": "21.0",
      "price": 2199,
      "isNotAvailable": true,
      "availableInStores": []
    },
    {
      "skuId": "4216662",
      "size": "L",
      "grossWeight": "21.1",
      "price": 2199,
      "isNotAvailable": true,
      "availableInStores": []
    },
    {
      "skuId": "4216664",
      "size": "XL",
      "grossWeight": "21.2",
      "price": 2199,
      "availableInStores": []
    }
  ]
}

La taille qui m'intéresse est donc le SKU 4216662.

Stock des magasins

Vient ensuite la partie sur le stock des magasins. Je clique sur le bouton permettant de vérifier le stock des magasins, rentre mon code postal, et je me retrouve avec la liste des magasins.

Je regarde les requêtes et trouve celle qui m'intéresse avec l'ensemble des magasins du nord de la france:

GET /fr/ajax/rest/model/com/decathlon/cube/commerce/inventory/InventoryActor/getStoreAvailability?storeIds=0070000200002%2C0070093300933%2C0070043700437%2C0070051800518%2C0070011800118%2C0070219902199%2C0070050400504%2C0070253902539%2C0070001500015%2C0070064800648&skuId=4216662&modelId=8614842&displayStoreDetails=false

Et avec la réponse:

{
  "responseTO": {
    "data": [
      {
        "aboveThreshold": false,
        "address": null,
        "availabilityInfo": "noStock",
        "clickNcollect1h": false,
        "favoriteStore": false,
        "latitude": 0,
        "longitude": 0,
        "optionId": null,
        "originId": null,
        "phoneNumber": null,
        "priceId": null,
        "quantity": 0,
        "securedStockLevel": 0,
        "skuId": "4216662",
        "storeId": "0070000200002",
        "storeName": "Neuville en Ferrain - Roncq",
        "storeSchedule": null,
        "storeUrl": null
      },
      {
        "aboveThreshold": false,
        "address": null,
        "availabilityInfo": "noStock",
        "clickNcollect1h": false,
        "favoriteStore": false,
        "latitude": 0,
        "longitude": 0,
        "optionId": null,
        "originId": null,
        "phoneNumber": null,
        "priceId": null,
        "quantity": 0,
        "securedStockLevel": 1,
        "skuId": "4216662",
        "storeId": "0070051800518",
        "storeName": "Marcq-en-Baroeul DOMYOS",
        "storeSchedule": null,
        "storeUrl": null
      },
      {
        "aboveThreshold": false,
        "address": null,
        "availabilityInfo": "noStock",
        "clickNcollect1h": false,
        "favoriteStore": false,
        "latitude": 0,
        "longitude": 0,
        "optionId": null,
        "originId": null,
        "phoneNumber": null,
        "priceId": null,
        "quantity": 0,
        "securedStockLevel": 1,
        "skuId": "4216662",
        "storeId": "0070219902199",
        "storeName": "Villeneuve d'Ascq - DX",
        "storeSchedule": null,
        "storeUrl": null
      },
      ...
    ],
  }
}

Parfait, j'ai l'ensemble des requêtes et le résultat permettant de recupérer les informations dont j'ai besoin. Il ne me reste plus qu'à coder. Je crée donc un premier service dont le but est pour un code donné de récupérer le stock internet et les stocks en magasin.

Il est probable que j'aurais pu mieux écrire mon code (et d'ailleurs mieux utiliser RxJS), mais je vous rappelle, ceci est un 1er jet. Le but étant de déployer cela le plus rapidement possible.

@Injectable()
export class AppService {
  constructor(private httpService: HttpService) {}

  async getVtcElectectricL(code: string): Promise<StockInformation[]> {
    const availability = [];
    const sku = await firstValueFrom(
      this.httpService.get<StocksOnline>(STOCKS_ONLINE([code])),
    );
    const onlineStocks = sku.data[code]?.stockOnline;

    availability.push({
      type: 'Internet',
      stocks: onlineStocks,
    });

    const stores = await firstValueFrom(
      this.httpService.get<StocksStores>(STOCKS_STORE(code)),
    );

    availability.push(
      ...stores.data.responseTO.data
        .map((store) => ({
          type: store.storeName,
          stocks: store.quantity,
        }))
        .sort((a, b) => b.stocks - a.stocks),
    );

    return availability.map((store) => ({
      ...store,
      availability: store.stocks > 0 ? 'available' : 'not_available',
    }));
  }
}

Une petite page en Mustashe pour afficher en HTML le contenu de la page et c'est parti !

La page WEB

Le déploiement

Afin d'améliorer mes compétences professionnelles je décide de déployer cette application sur AWS, et afin que cette application me coûte le moins cher possible, je vais déployer cette application dans une Lambda à l'aide du framework Serverless.

Viens alors l'adaptation du l'application Nest.JS pour utiliser le framework Serverless. Je laisse la documentation de Nest.JS expliquer comment faire: Nest.JS - Serverless.

service: triathlon
frameworkVersion: "2"

plugins:
  - serverless-plugin-log-retention
  - serverless-offline
  - serverless-domain-manager

custom:
  region: eu-central-1
  logRetentionInDays: 7
  serverless-offline:
    noPrependStageInUrl: true
    allowCache: false
  customDomain:
    domainName: "decat-${self:provider.stage}.aws.shadoware.org"
    basePath: ""
    certificateName: "aws.shadoware.org"
    createRoute53Record: true
    endpointType: "regional"
    securityPolicy: tls_1_2
    apiType: rest
    autoDomain: false

provider:
  name: aws
  region: ${self:custom.region}
  runtime: nodejs12.x
  lambdaHashingVersion: "20201221"
  stage: ${opt:stage, 'dev'}
  environment:
    NODE_OPTIONS: "--max-http-header-size=80000"

package:
  patterns:
    - "!./**"
    - dist/**
    - views/**

functions:
  api:
    handler: dist/main.handler
    environment:
      region: ${self:custom.region}
    events:
      - http:
          path: /
          method: ANY
      - http:
          path: /{proxy+}
          method: ANY

Il ne reste plus qu'à lancer le déploiement:

npx serverless deploy

Nous avons alors notre belle page de déployée.

La suite de l'aventure

Me voilà avec un petit programme qui me permet de facilement visualiser les stocks du vélo que je souhaite. J'en profite également pour ajouter le même produit en taille XL (où il y a du stock) et un VTT.

Le problème c'est que je dois continuer à rafraîchir la page, même si c'est plus rapide.

Pour l'anecdocte, après une matinée de glandouille (c'est les vacances), je décide de rafraîchir la page vers midi. Je vois "Stock: 1". Heureux, je me précipite pour acheter le seul exemplaire. Mais lors de la commande je tombe sur le fameux message: "Internal Server Error" dans un bandeau rouge en haut de l'écran.

J'appelle le service client qui m'informe que cela provient du fait qu'il n'y a déjà plus de stock mais que le stock du site n'est mis à jours que tout les 24h. Je m'en doutais mais le message d'erreur laisse à désirer un peu.

Malgrès l'apparition soudaine d'un stock, je n'ai recu aucun mail pour me prévenir (relatif à la fonctionnalité "M'avertir lorsque le produit est à nouveau disponible"). Il me faut un moyen d'être averti dans les premiers dès qu'il y a du stock.

La situation s'est reproduite sans que je sois averti par mail.

Bref, il me faut une solution !

La notification

Je profite donc des services AWS pour développer une nouvelle fonctionnalité. Cette dernière est simple: je vais faire une requête à mon service toutes les 30 minutes, pour vérifier le stock, et si ce dernier bouge, j'envoie un SMS.

Le coût actuel d'envoi d'un SMS vers la France sur AWS est de 0.06933$. Donc si je recois 1 SMS par heure en journée, je vais aller vers 1$ par jour. C'est à prévoir pour mon budget. Maintenant je veux recevoir le minimum de SMS. Je n'ai pas envie de recevoir de SMS quand le stock ne change pas. Il faut donc que je puisse comparer le stock entre l'état précédent et l'etat actuel.

Quand on utilise les lambda (fonction) d'AWS, il n'y a pas de serveur de démarré. Donc je ne peux pas stocker les valeurs précédentes temporairement en mémoire pour contrôler les changements (ce qui ne serait pas une bonne pratique, mais un truc rapide à faire pour un dev maison).

Je stocke donc le dernier accès au site dans une base de données de type Document de chez AWS: DynamoDB.

Le service d'envoi de SMS est simple (et prend un numéro de téléphone et un message):

  /**
   * Sends a SMS message to the specificed phone number.
   * Returns the message ID from SNS
   * @param   {String} phoneNumber
   * @param   {String} message
   * @returns {Promise|String}
   */
  async sendMessage(phoneNumber, message) {
    await this.SNS.setSMSAttributes({
      attributes: { DefaultSMSType: 'Promotional' },
    }).promise();

    const smsData = {
      Message: message,
      PhoneNumber: phoneNumber,
      MessageAttributes: {
        'AWS.SNS.SMS.SenderID': {
          DataType: 'String',
          StringValue: 'DecatStock',
        },
      },
    };

    this.logger.log(
      `Send message to ${phoneNumber} (${message.length}):\n${message}`,
    );
    const response = await this.SNS.publish(smsData).promise();

    this.logger.log(`Response of SMS is ${JSON.stringify(response)}`);
    return response.MessageId;
  }

Le service permettant de lire et d'écrire dans la base DynamoDB est lui aussi très simple et ne possède que deux méthodes:

class {
  async findOneById(sku: string): Promise<DecathlonStatus> {
    try {
      const result = await dynamoDB
        .get({
          TableName: process.env.DECATHLON_TABLE_NAME,
          Key: { id: sku },
        })
        .promise();
      return result.Item as DecathlonStatus;
    } catch (error) {
      this.logger.warn(`Can't read the table ${sku}: ${error.message}`);
      return null;
    }
  }

  async update(
    sku: string,
    items: StockInformations
  ): Promise<DecathlonStatus> {
    const status = new DecathlonStatus();
    status.id = sku;
    status.informations = items;

    try {
      await dynamoDB
        .put({
          TableName: process.env.DECATHLON_TABLE_NAME,
          Item: status,
        })
        .promise();
      return status;
    } catch (error) {
      throw new InternalServerErrorException(error);
    }
  }
}

Le but est bien de lire le status et d'écrire le status pour un produit.

Le service qui sera appellé de façon régulière est relativement simple. Il va:

  1. appeler le service d'API REST pour vérifier le stock en live,
  2. appeler la service DynamoDB pour récupérer le dernier stock en base de données,
  3. et faire la différences entre les deux, et s'il y en a une: envoyer un SMS.
export class AvailibilityCheckerService {
  private logger = new Logger(AvailibilityCheckerService.name);

  constructor(
    private service: AppService,
    private decathlonService: DecathlonsService,
    private smsService: SMSService,
  ) {}

  async checkAvailibility() {
    const message = (
      await Promise.all(
        PRODUCTS.map(
          async (product) =>
            await this.checkAvailabilityOfSku(product.code, product.name),
        ),
      )
    )
      .filter((message) => !!message)
      .join('\n\n');

    if (message.length) {
      await this.smsService.sendMessage(process.env.PHONE_NUMBER, message);
    }
  }

  async checkAvailabilityOfSku(code: string, name: string) {
    const stocks = await this.service.getVtcElectectricL(code);
    const oldStocks = await this.decathlonService.findOneById(code);
    const diffStocks = this.diffStocks(oldStocks?.informations, stocks);
    this.logger.debug(`The are ${diffStocks.length} stocks that change`);

    let message;
    if (diffStocks.length) {
      message = diffStocks
        .map((value) => `  ${value.type} - ${value.stocks}`)
        .join('\n');
    }

    await this.decathlonService.update(code, stocks);
    return message && `${name}:\n${message}`;
  }

  diffStocks(oldInfos: StockInformations, newInfos: StockInformations) {
    oldInfos || (oldInfos = []);
    newInfos || (newInfos = []);

    const oldMap = oldInfos.reduce((acc, i) => {
      acc[i.type] = i;
      return acc;
    }, {} as Record<string, StockInformation>);

    return newInfos.filter((info) => {
      const oldStocks = oldMap[info.type]?.stocks || 0;
      return oldStocks !== (info?.stocks || 0);
    });
  }
}

Cela me permet de recevoir un SMS de la forme :

Les SMS

Déploiement de la notification.

Pour déployer la notification, il me faut deux endpoints (un pour la partie web, et l'autre pour la notification).

Je garde donc dans le dossier src le fichier main.ts pour la partie web et je créé un point d'entrée spécial pour la lambda de notification:

import { HttpStatus } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Handler } from 'aws-lambda';

import { CheckAvailabilityModule } from './check-availability.module';
import { AvailibilityCheckerService } from './availability/availibility-checker.service';

let service: AvailibilityCheckerService;

async function bootstrap(): Promise<AvailibilityCheckerService> {
  const appContext = await NestFactory.createApplicationContext(
    CheckAvailabilityModule,
  );
  return appContext.get(AvailibilityCheckerService);
}

export const handler: Handler = async () => {
  service = service ?? (await bootstrap());

  await service.checkAvailibility();

  return {
    statusCode: HttpStatus.OK,
  };
};

Contrairement à la partie WEB ce service ne démarre pas de serveur Web (ni la partie express).

Le paramétrage de la lambda se fait donc avec le fichier serverless.yml dont voici les principales modifications:

service: decathlon
frameworkVersion: "2"

plugins:
  - serverless-dynamodb-local
  - serverless-plugin-log-retention
  - serverless-offline
  - serverless-domain-manager

custom:
  region: eu-central-1
  logRetentionInDays: 7
  serverless-offline:
    noPrependStageInUrl: true
    allowCache: false
  decathlonTable:
    name: ${self:provider.stage}-decathlon
    arn: !GetAtt decathlonTable.Arn
  dynamodb:
    stages:
      - ${self:provider.stage}
    start:
      migrate: true
  endpoints:
    dynamodbURL: "http://localhost:8000"
  customDomain: ...

provider:
  name: aws
  region: ${self:custom.region}
  runtime: nodejs12.x
  lambdaHashingVersion: "20201221"
  stage: ${opt:stage, 'dev'}
  environment:
    DYNAMODB_ENDPOINT: ${self:custom.endpoints.dynamodbURL}
    DECATHLON_TABLE_NAME: ${self:custom.decathlonTable.name}
    NODE_OPTIONS: "--max-http-header-size=80000"
  iamRoleStatements: ${file(delivery/roles.yml)}

package:
  individually: true
  patterns:
    - "!./**"

functions:
  api:
    handler: dist/main.handler
    environment:
      region: ${self:custom.region}
    events:
      - http:
          path: /
          method: ANY
      - http:
          path: /{proxy+}
          method: ANY
    package:
      patterns:
        - dist/main.js
        - views/**

  check-availability:
    handler: dist/check-availability.handler
    events:
      - schedule: rate(30 minutes)
    package:
      patterns:
        - dist/check-availability.js

resources:
  Resources:
    decathlonTable: ${file(delivery/decathlonTable.yml):decathlonTable}

Il ne reste plus qu'à déployer et profiter de l'envoi de SMS.

Optimisation

Sur AWS, le coût d'une lambda est lié au nombre d'exécutions mais aussi au temps d'exécution. Il est important que le temps d'exécution soit le plus rapide possible.

Ici dans notre cas le temps le temps de traitement sera lié au temps de réponse de la requête HTTP extérieur à notre site. Afin de parfaire notre petite application de bonne pratique, nous allons utiliser webpack (côté serveur) afin d'optimiser le temps de démarrage de notre application.

Grâce au framework Nest.JS, cela est très facile à faire, dans le fichier nest-cli.json, nous allons demander à Nest.JS de générer un bundle webpack:

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "webpack": true
  }
}

Par défaut, webpack ne crée un bundle qu'avec le source de notre application (et donc sans les modules node). Ce qui nous intéresse c'est de faire un bundle comprenant notre application ainsi que les modules node (on peut voir un bench sur la FAQ Nest.JS).

Pour cela nous allons créer un fichier webpack.config.js qui sera pris automatiquement (inspiré de la documentation mais modifié pour posséder plusieurs points d'entrées, on exclut aussi le framework AWS qui est déjà inclus dans AWS):

module.exports = (options, webpack) => {
  const lazyImports = [
    "@nestjs/microservices/microservices-module",
    "@nestjs/websockets/socket-module",
  ];

  return {
    ...options,
    entry: {
      main: options.entry,
      "check-availability": options.entry.replace("main", "check-availability"),
    },
    externals: ["aws-sdk"],
    output: {
      filename: "[name].js",
      libraryTarget: "commonjs2",
    },
    plugins: [
      ...options.plugins,
      new webpack.IgnorePlugin({
        checkResource(resource) {
          if (lazyImports.includes(resource)) {
            try {
              require.resolve(resource);
            } catch (err) {
              return true;
            }
          }
          return false;
        },
      }),
    ],
  };
};

A l'heure actuelle (mais principalement à cause du temps d'exécution de la requête), la requête prend environ 1s:

REPORT RequestId: eaa2b5a3-a754-4b1f-adcc-22ca3c6045aa Duration: 984.41 ms Billed Duration: 985 ms Memory Size: 1024 MB Max Memory Used: 109 MB Init Duration: 540.30 ms

Le coût du projet

Grâce au Free-Tiers d'AWS, ce qui me coûte réellement de l'argent dans ce projet est SNS (qui sert à l'envoi de SMS). L'autre coût est relatif à Route 53 car j'ai déployé le site dans un sous-domaine.

Au 29 Août 2021, je vais avoir dépensé 7,34\$ pour me faire plaisir (j'aurais pu le déployer sur mes propres serveurs dédiés) et pour pouvoir m'acheter un vélo sans attendre que le site m'envoie un mail ou qu'il y ait suffisamment de stock pendant suffisamment longtemps pour que je puisse voir le vélo, ou l'acheter en magasin.

Coût du projet

Bilan

On est fin septembre, j'ai reçu mon vélo et j'ai pu rouler plusieurs semaines avec (Je l'ai recu début septembre).

Cela a commencé par une notification à 11h59 indiquant que 4 exemplaires avaient été ajoutés au site. J'ai pu le commander dans la foulée. A 14h, tous les exemplaires avaient été vendus. Je devais partir manger quand j'ai reçu la notification.

Si je n'avais pas développé ce petit programme, je n'aurais jamais su que des vélos étaient en stock, car je n'ai jamais reçu le fameux mail. De plus en travaillant (car je suis revenu de vacances depuis) je ne peux pas rafraîchir la page en continu (1 fois le matin, 1 fois le soir).

Bref, est-ce que le mail que doit nous envoyer le site est de la poudre aux yeux ? Se sert-il d'une file d'attente (et dans ce cas je m'excuse auprès de celui que j'ai doublé) ? Ou n'est-il envoyé que quand il y a plus de 10 exemplaires qui arrivent sur le site ?

Depuis j'ai désactivé le programme. Avant j'avais pu voir le stock bouger rapidement sur les VTT en XL et sur le VTC en XL et en S. Ce que je peux dire c'est qu'une trentaine de vélo ajoutés sur le VTT le soir à 21h partent comme des petits pains. Il n'en restait plus le lendemin à 12h...

Au delà de l'achat du vélo, j'ai été content de pouvoir mettre un site en production rapidement et quasi-gratuitement (si on ne compte pas les SMS). Je me demande ce que m'aurais coûté le même site au delà de la période des 1 an (après la fin du free-tiers).


  1. Certains me conseillent des petits magasins de vélo, d'autres des VTCAE à plus de 5 000 €. Bref, nous ne sommes pas là pour parler de mon choix d'enseigne. Mais si vous voulez tout savoir, c'est une question pratique. Il est à coté de chez moi.

  2. Hello friend jsdom only handles static html, to make the click event you will need a headless browser for scraping the web. There are several one of them is the Puppeter that uses the chorme in the background.