src/App/Entity/ApplicationEvent.php line 41

Open in your IDE?
  1. <?php
  2. namespace App\Entity;
  3. use App\Utility\DateTimeUtility;
  4. use App\Utility\GuidUtility;
  5. use App\Utility\ReflectionHelper;
  6. use DateTime;
  7. use Doctrine\ORM\Mapping as ORM;
  8. use Exception;
  9. use ReflectionClass;
  10. use ValueError;
  11. /**
  12. * @ORM\Entity(repositoryClass="App\Repository\ApplicationEventRepository")
  13. *
  14. * @ORM\Table(
  15. * name="application_events",
  16. * indexes={
  17. *
  18. * @ORM\Index(name="occured_at_idx", columns={"occured_at"}),
  19. * @ORM\Index(name="event_type_occured_at_idx", columns={"event_type", "occured_at"}),
  20. * @ORM\Index(name="request_id_occured_at_idx", columns={"request_id", "occured_at"}),
  21. * @ORM\Index(name="session_id_occured_at_idx", columns={"session_id", "occured_at"}),
  22. * @ORM\Index(name="jobofferer_idx", columns={"affected_user_is_jobofferer", "occured_at", "affected_user_registered_at", "event_type"}),
  23. * @ORM\Index(name="jobseeker_idx", columns={"affected_user_is_jobseeker", "occured_at", "affected_user_registered_at", "event_type"})
  24. * }
  25. * )
  26. *
  27. * This model deliberately stores its data in a strongly denormalized form in order to allow
  28. * efficient retrieval, searching, and grouping for statistical purposes, therefore avoiding
  29. * cross-references.
  30. *
  31. * IMPORTANT: Events of this type MUST NOT be used for non-statistical uses. Specifically, DO NOT use this data to make
  32. * functional application decisions ("if there are more than X events of type Y for a user, do Z").
  33. * Always treat this data as if it wasn't part of the application at all - because this might well become true in the
  34. * future, when we need to move this data into a dedicated data warehouse for large-scale statistical analytics.
  35. *
  36. * If you need to base application behaviour on past user behaviour,@see UsageEventService instead.
  37. */
  38. class ApplicationEvent
  39. {
  40. public const EVENT_CATEGORY_INFO = 0; // e.g. "an email was queued for delivery", "the user submitted the profile editor with invalid entries"
  41. public const EVENT_CATEGORY_ERROR = 1; // e.g. "could not find a user for the given user id on the billwerk return page"
  42. public const EVENT_CATEGORY_METRIC = 2; // e.g. "notification job started and found <metric> users to notify", "notification job finished after <metric> seconds"
  43. public const EVENT_TYPE_QUEUED_MAIL_FOR_SENDING = 0;
  44. public const EVENT_TYPE_MAIL_DELIVERY_REPORTED = 1;
  45. public const EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_SUCCESSFUL = 2;
  46. public const EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_UNSUCCESSFUL = 3;
  47. public const EVENT_TYPE_EXCEPTION = 4;
  48. public const EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_INCOMING_DIRECT_EMAIL_COMMUNICATION = 5;
  49. public const EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_DIRECT_EMAIL_COMMUNICATION_MESSAGE_EXCHANGE_BETWEEN_TWO_PROFILES = 6;
  50. public const EVENT_TYPE_GOHIRING_INCOMING_NEW_FAILED = 7;
  51. public const EVENT_TYPE_GOHIRING_INCOMING_UPDATE_FAILED = 8;
  52. public const EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED = 9;
  53. public const EVENT_TYPE_SELFDESCRIPTION_PROPOSAL_ASSIGNED = 10;
  54. public const EVENT_TYPE_GERMAN_PERSONNEL_IMPORT_FAILED = 11;
  55. public const EVENT_TYPE_REWE_IMPORT_FAILED = 12;
  56. public const EVENT_TYPE_HAYS_IMPORT_FAILED = 13;
  57. public const EVENT_TYPE_MYSTAFFPILOT_IMPORT_FAILED = 14;
  58. public const EVENT_TYPE_SOFTGARDEN_IMPORT_FAILED = 15;
  59. public const EVENT_TYPE_INVALID_MAIL_ADDRESS_FOR_MAILING_DETECTED = 16;
  60. public const EVENT_TYPE_LENKZEIT_IMPORT_FAILED = 17;
  61. public const EVENT_TYPE_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED = 18;
  62. public const EVENT_TYPE_CAPTCHA_RETURNED_ERROR = 19;
  63. public const EVENT_TYPE_ARTEMIS_IMPORT_FAILED = 20;
  64. public const EVENT_TYPE_FLASCHENPOST_IMPORT_FAILED = 21;
  65. public const EVENT_TYPE_DIRECT_EMAIL_COMMUNICATION_INCOMING_MESSAGE_BLOCKED_BASED_ON_KEYPHRASE = 22;
  66. public const EVENT_TYPE_FEED_WAS_NOT_UPDATED = 23;
  67. public const EVENT_TYPE_DEUTSCHE_BAHN_AG_IMPORT_FAILED = 24;
  68. public const EVENT_TYPE_MILCH_UND_ZUCKER_IMPORT_FAILED = 25;
  69. public const EVENT_TYPE_RECURRENT_JOB_SHARE_PAGE_WAS_REQUESTED_BY_BOT = 26;
  70. public const EVENT_TYPE_CONCLUDIS_API_ERROR = 27;
  71. public const EVENT_TYPE_KAUFLAND_IMPORT_FAILED = 28;
  72. public const EVENT_TYPE_CELLIT_IMPORT_FAILED = 29;
  73. public const EVENT_TYPE_CONTENT_PARSER_TASK_ERROR = 30;
  74. public const EVENT_TYPE_AFA_UPLOAD_CHECKER_RETURNED_RESULT = 31;
  75. public const EVENT_TYPE_ADECCO_FEED_IMPORT_FAILED = 32;
  76. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_SUCCEEDED = 33;
  77. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_ERRORED = 34;
  78. public const EVENT_TYPE_INDEED_FEED_CREATION_FROM_FILE_RECURRENT_JOB_REJECTED = 35;
  79. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_SUCCEEDED = 36;
  80. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_ERRORED = 37;
  81. public const EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_SUCCEEDED = 38;
  82. public const EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_ERRORED = 39;
  83. public const EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_QUOTA = 40;
  84. public const EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED = 41;
  85. public const EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_STATUS_FORBIDDEN = 42;
  86. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_SUCCEEDED = 43;
  87. public const EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_ERRORED = 44;
  88. public const EVENT_TYPE_WECLAPP_API_ERROR = 45;
  89. public const EVENT_TYPE_SOFTGARDEN_API_ERROR = 46;
  90. public const EVENT_TYPE_AI_AFA_ASSIGNMENT_BOTH_MATCHED = 47;
  91. public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_MATCH = 48;
  92. public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_PROFESSION_MATCH = 49;
  93. public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ECONOMIC_SECTOR_MATCH = 50;
  94. public const EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ASSIGNMENTS = 51;
  95. public const EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_LIMIT_EXHAUSTED = 52;
  96. public const ORDERED_EVENT_TYPES = [
  97. self::EVENT_TYPE_QUEUED_MAIL_FOR_SENDING,
  98. self::EVENT_TYPE_MAIL_DELIVERY_REPORTED,
  99. self::EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_SUCCESSFUL,
  100. self::EVENT_TYPE_MAIL_DELIVERY_REPORTED_AS_UNSUCCESSFUL,
  101. self::EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_INCOMING_DIRECT_EMAIL_COMMUNICATION,
  102. self::EVENT_TYPE_CIRCUITBREAKER_OPENED_FOR_DIRECT_EMAIL_COMMUNICATION_MESSAGE_EXCHANGE_BETWEEN_TWO_PROFILES,
  103. self::EVENT_TYPE_GOHIRING_INCOMING_NEW_FAILED,
  104. self::EVENT_TYPE_GOHIRING_INCOMING_UPDATE_FAILED,
  105. self::EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED,
  106. self::EVENT_TYPE_SELFDESCRIPTION_PROPOSAL_ASSIGNED,
  107. self::EVENT_TYPE_GOHIRING_INCOMING_DELETE_FAILED,
  108. self::EVENT_TYPE_GERMAN_PERSONNEL_IMPORT_FAILED,
  109. self::EVENT_TYPE_REWE_IMPORT_FAILED,
  110. self::EVENT_TYPE_HAYS_IMPORT_FAILED,
  111. self::EVENT_TYPE_MYSTAFFPILOT_IMPORT_FAILED,
  112. self::EVENT_TYPE_SOFTGARDEN_IMPORT_FAILED,
  113. self::EVENT_TYPE_INVALID_MAIL_ADDRESS_FOR_MAILING_DETECTED,
  114. self::EVENT_TYPE_LENKZEIT_IMPORT_FAILED,
  115. self::EVENT_TYPE_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED,
  116. self::EVENT_TYPE_CAPTCHA_RETURNED_ERROR,
  117. self::EVENT_TYPE_ARTEMIS_IMPORT_FAILED,
  118. self::EVENT_TYPE_FLASCHENPOST_IMPORT_FAILED,
  119. self::EVENT_TYPE_DIRECT_EMAIL_COMMUNICATION_INCOMING_MESSAGE_BLOCKED_BASED_ON_KEYPHRASE,
  120. self::EVENT_TYPE_FEED_WAS_NOT_UPDATED,
  121. self::EVENT_TYPE_DEUTSCHE_BAHN_AG_IMPORT_FAILED,
  122. self::EVENT_TYPE_MILCH_UND_ZUCKER_IMPORT_FAILED,
  123. self::EVENT_TYPE_RECURRENT_JOB_SHARE_PAGE_WAS_REQUESTED_BY_BOT,
  124. self::EVENT_TYPE_CONCLUDIS_API_ERROR,
  125. self::EVENT_TYPE_KAUFLAND_IMPORT_FAILED,
  126. self::EVENT_TYPE_CELLIT_IMPORT_FAILED,
  127. self::EVENT_TYPE_AFA_UPLOAD_CHECKER_RETURNED_RESULT,
  128. self::EVENT_TYPE_ADECCO_FEED_IMPORT_FAILED,
  129. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_SUCCEEDED,
  130. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_SOFTGARDEN_API_ERRORED,
  131. self::EVENT_TYPE_INDEED_FEED_CREATION_FROM_FILE_RECURRENT_JOB_REJECTED,
  132. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_SUCCEEDED,
  133. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_RANGER_API_ERRORED,
  134. self::EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_SUCCEEDED,
  135. self::EVENT_TYPE_SENDING_ADDITIONAL_CV_FILES_TO_RANGER_API_ERRORED,
  136. self::EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_QUOTA,
  137. self::EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_HANDLING_FAILED,
  138. self::EVENT_TYPE_POTENTIAL_CUSTOMER_BUSINESS_INFO_CRAWLER_CONTENT_PARSER_RESULT_STATUS_FORBIDDEN,
  139. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_SUCCEEDED,
  140. self::EVENT_TYPE_SENDING_EXTERNAL_APPLICATION_TO_ZVOOVE_API_ERRORED,
  141. self::EVENT_TYPE_WECLAPP_API_ERROR,
  142. self::EVENT_TYPE_SOFTGARDEN_API_ERROR,
  143. self::EVENT_TYPE_AI_AFA_ASSIGNMENT_BOTH_MATCHED,
  144. self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_MATCH,
  145. self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_PROFESSION_MATCH,
  146. self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ECONOMIC_SECTOR_MATCH,
  147. self::EVENT_TYPE_AI_AFA_ASSIGNMENT_NO_ASSIGNMENTS,
  148. self::EVENT_TYPE_RECURRENT_JOB_IMPORT_FAILED_BECAUSE_OF_LIMIT_EXHAUSTED
  149. ];
  150. /**
  151. * @throws Exception
  152. */
  153. public function __construct(
  154. int $eventCategory,
  155. int $eventType
  156. ) {
  157. if (!ReflectionHelper::hasConstWithValue(
  158. self::class,
  159. 'EVENT_CATEGORY_',
  160. $eventCategory
  161. )) {
  162. throw new ValueError("Unknown event category '$eventCategory'.");
  163. }
  164. if (!ReflectionHelper::hasConstWithValue(
  165. self::class,
  166. 'EVENT_TYPE_',
  167. $eventType
  168. )) {
  169. throw new ValueError("Unknown event type '$eventType'.");
  170. }
  171. $this->eventCategory = $eventCategory;
  172. $this->eventType = $eventType;
  173. $this->occuredAt = DateTimeUtility::createDateTimeUtc();
  174. }
  175. /**
  176. * @ORM\GeneratedValue(strategy="CUSTOM")
  177. *
  178. * @ORM\CustomIdGenerator(class="App\Utility\DatabaseIdGenerator")
  179. *
  180. * @ORM\Column(name="id", type="guid")
  181. *
  182. * @ORM\Id
  183. */
  184. private string $id;
  185. public function setId(string $id): void
  186. {
  187. GuidUtility::validOrThrow($id);
  188. $this->id = $id;
  189. }
  190. public function getId(): ?string
  191. {
  192. return $this->id;
  193. }
  194. /**
  195. * @ORM\Column(name="event_category", type="smallint", nullable=false)
  196. */
  197. private int $eventCategory;
  198. public function getEventCategory(): int
  199. {
  200. return $this->eventCategory;
  201. }
  202. /**
  203. * @throws Exception
  204. */
  205. public function getEventCategoryTitle(): string
  206. {
  207. $refl = new ReflectionClass(ApplicationEvent::class);
  208. $constants = $refl->getConstants();
  209. foreach ($constants as $constantName => $constantValue) {
  210. if (substr($constantName, 0, 15) === 'EVENT_CATEGORY_' && $constantValue === $this->getEventCategory()) {
  211. return strtolower(str_replace('_', '-', substr($constantName, 15)));
  212. }
  213. }
  214. throw new Exception('Cannot resolve title for event category ' . $this->getEventType());
  215. }
  216. /**
  217. * @ORM\Column(name="event_type", type="smallint", nullable=false)
  218. */
  219. private int $eventType;
  220. public function getEventType(): int
  221. {
  222. return $this->eventType;
  223. }
  224. /**
  225. * @throws Exception
  226. */
  227. public function getEventTypeTitle(): string
  228. {
  229. $refl = new ReflectionClass(ApplicationEvent::class);
  230. $constants = $refl->getConstants();
  231. foreach ($constants as $constantName => $constantValue) {
  232. if (substr($constantName, 0, 11) === 'EVENT_TYPE_' && $constantValue === $this->getEventType()) {
  233. return strtolower(str_replace('_', '-', substr($constantName, 11)));
  234. }
  235. }
  236. throw new Exception('Cannot resolve title for event type ' . $this->getEventType());
  237. }
  238. /**
  239. * @ORM\Column(name="occured_at", type="datetime", nullable=false)
  240. */
  241. private DateTime $occuredAt;
  242. public function setOccuredAt(DateTime $occuredAt): void
  243. {
  244. $this->occuredAt = $occuredAt;
  245. }
  246. public function getOccuredAt(): DateTime
  247. {
  248. return $this->occuredAt;
  249. }
  250. /**
  251. * @ORM\Column(name="affected_user_id", type="guid", nullable=true)
  252. *
  253. * We don't make this a foreign key on purpose, we don't need the integrity and don't want to delete event data
  254. * if a user is deleted.
  255. */
  256. private ?string $affectedUserId = null;
  257. public function setAffectedUserId(?string $userId = null): void
  258. {
  259. GuidUtility::validOrThrow($userId, true);
  260. $this->affectedUserId = $userId;
  261. }
  262. public function getAffectedUserId(): ?string
  263. {
  264. return $this->affectedUserId;
  265. }
  266. /**
  267. * @ORM\Column(name="affected_user_is_jobofferer", type="boolean", nullable=true)
  268. */
  269. private ?bool $affectedUserIsJobofferer = null;
  270. public function setAffectedUserIsJobofferer(?bool $affectedUserIsJobofferer): void
  271. {
  272. $this->affectedUserIsJobofferer = $affectedUserIsJobofferer;
  273. }
  274. public function getAffectedUserIsJobofferer(): ?bool
  275. {
  276. return $this->affectedUserIsJobofferer;
  277. }
  278. /**
  279. * @ORM\Column(name="affected_user_is_jobseeker", type="boolean", nullable=true)
  280. */
  281. private ?bool $affectedUserIsJobseeker = null;
  282. public function setAffectedUserIsJobseeker(?bool $affectedUserIsJobseeker): void
  283. {
  284. $this->affectedUserIsJobseeker = $affectedUserIsJobseeker;
  285. }
  286. public function getAffectedUserIsJobseeker(): ?bool
  287. {
  288. return $this->affectedUserIsJobseeker;
  289. }
  290. /**
  291. * @ORM\Column(name="affected_user_registered_at", type="datetime", nullable=true)
  292. *
  293. * In order to show statistics related to the cohorte of all users registered on day X, we need this field
  294. *
  295. * E.g. "from all user registered on 2018-04-07, how many ran into error X?"
  296. */
  297. private ?DateTime $affectedUserRegisteredAt = null;
  298. public function setAffectedUserRegisteredAt(?DateTime $affectedUserRegisteredAt = null): void
  299. {
  300. $this->affectedUserRegisteredAt = $affectedUserRegisteredAt;
  301. }
  302. public function getAffectedUserRegisteredAt(): ?DateTime
  303. {
  304. return $this->affectedUserRegisteredAt;
  305. }
  306. /**
  307. * @ORM\Column(name="metric", type="float", nullable=true)
  308. */
  309. private ?float $metric = null;
  310. /**
  311. * @throws Exception
  312. */
  313. public function setMetric(?float $metric = null): void
  314. {
  315. $this->metric = $metric;
  316. }
  317. public function getMetric(): ?float
  318. {
  319. return $this->metric;
  320. }
  321. /**
  322. * @ORM\Column(name="error_message", type="text", length=8192, nullable=true)
  323. */
  324. private ?string $errorMessage = null;
  325. /**
  326. * @throws Exception
  327. */
  328. public function setErrorMessage(?string $errorMessage = null): void
  329. {
  330. $this->errorMessage = $errorMessage;
  331. }
  332. public function getErrorMessage(): ?string
  333. {
  334. return $this->errorMessage;
  335. }
  336. /**
  337. * @ORM\Column(name="additional_data", type="text", length=8192, nullable=true)
  338. */
  339. private ?string $additionalData = null;
  340. /**
  341. * @throws Exception
  342. */
  343. public function setAdditionalData(?string $additionalData = null): void
  344. {
  345. if (!is_null($additionalData)) {
  346. if (is_null(json_decode($additionalData))) {
  347. throw new Exception('additionalData must be valid JSON');
  348. }
  349. }
  350. $this->additionalData = $additionalData;
  351. }
  352. public function getAdditionalData(): ?string
  353. {
  354. return $this->additionalData;
  355. }
  356. /**
  357. * @ORM\Column(name="request_id", type="text", length=256, nullable=true)
  358. */
  359. private ?string $requestId = null;
  360. public function setRequestId(?string $requestId = null): void
  361. {
  362. $this->requestId = $requestId;
  363. }
  364. public function getRequestId(): ?string
  365. {
  366. return $this->requestId;
  367. }
  368. /**
  369. * @ORM\Column(name="session_id", type="text", length=256, nullable=true)
  370. */
  371. private ?string $sessionId = null;
  372. public function setSessionId(?string $sessionId = null): void
  373. {
  374. $this->sessionId = $sessionId;
  375. }
  376. public function getSessionId(): ?string
  377. {
  378. return $this->sessionId;
  379. }
  380. /**
  381. * @ORM\Column(name="client_id", type="text", length=64, nullable=true)
  382. */
  383. private ?string $clientId = null;
  384. public function setClientId(?string $clientId = null): void
  385. {
  386. $this->clientId = $clientId;
  387. }
  388. public function getClientId(): ?string
  389. {
  390. return $this->clientId;
  391. }
  392. /**
  393. * @ORM\Column(name="is_probably_bot_request", type="boolean", nullable=true)
  394. */
  395. private ?bool $isProbablyBotRequest = null;
  396. public function getIsProbablyBotRequest(): ?bool
  397. {
  398. return $this->isProbablyBotRequest;
  399. }
  400. public function setIsProbablyBotRequest(?bool $isProbablyBotRequest): void
  401. {
  402. $this->isProbablyBotRequest = $isProbablyBotRequest;
  403. }
  404. }