visit.queries.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. const { isAfter, subDays, subHours, set, format } = require("date-fns");
  2. const utils = require("../utils");
  3. const redis = require("../redis");
  4. const knex = require("../knex");
  5. const env = require("../env");
  6. async function add(params) {
  7. const data = {
  8. ...params,
  9. country: params.country.toLowerCase(),
  10. referrer: params.referrer.toLowerCase()
  11. };
  12. const nowUTC = new Date().toISOString();
  13. const truncatedNow = nowUTC.substring(0, 10) + " " + nowUTC.substring(11, 14) + "00:00";
  14. return knex.transaction(async (trx) => {
  15. // Create a subquery first that truncates the
  16. const subquery = trx("visits")
  17. .select("visits.*")
  18. .select({
  19. created_at_hours: utils.knexUtils(trx).truncatedTimestamp("created_at", "hour")
  20. })
  21. .where({ link_id: data.link_id })
  22. .as("subquery");
  23. const visit = await trx
  24. .select("*")
  25. .from(subquery)
  26. .where("created_at_hours", "=", truncatedNow)
  27. .forUpdate()
  28. .first();
  29. if (visit) {
  30. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  31. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  32. await trx("visits")
  33. .where({ id: visit.id })
  34. .increment(`br_${data.browser}`, 1)
  35. .increment(`os_${data.os}`, 1)
  36. .increment("total", 1)
  37. .update({
  38. updated_at: utils.dateToUTC(new Date()),
  39. countries: JSON.stringify({
  40. ...countries,
  41. [data.country]: (countries[data.country] ?? 0) + 1
  42. }),
  43. referrers: JSON.stringify({
  44. ...referrers,
  45. [data.referrer]: (referrers[data.referrer] ?? 0) + 1
  46. })
  47. });
  48. } else {
  49. // This must also happen in the transaction to avoid concurrency
  50. await trx("visits").insert({
  51. [`br_${data.browser}`]: 1,
  52. countries: { [data.country]: 1 },
  53. referrers: { [data.referrer]: 1 },
  54. [`os_${data.os}`]: 1,
  55. total: 1,
  56. link_id: data.link_id,
  57. user_id: data.user_id,
  58. });
  59. }
  60. return visit;
  61. });
  62. }
  63. async function find(match, total) {
  64. if (match.link_id && env.REDIS_ENABLED) {
  65. const key = redis.key.stats(match.link_id);
  66. const cached = await redis.client.get(key);
  67. if (cached) return JSON.parse(cached);
  68. }
  69. const stats = {
  70. lastDay: {
  71. stats: utils.getInitStats(),
  72. views: new Array(24).fill(0),
  73. total: 0
  74. },
  75. lastWeek: {
  76. stats: utils.getInitStats(),
  77. views: new Array(7).fill(0),
  78. total: 0
  79. },
  80. lastMonth: {
  81. stats: utils.getInitStats(),
  82. views: new Array(30).fill(0),
  83. total: 0
  84. },
  85. lastYear: {
  86. stats: utils.getInitStats(),
  87. views: new Array(12).fill(0),
  88. total: 0
  89. }
  90. };
  91. const visitsStream = knex("visits").where(match).stream();
  92. const now = new Date();
  93. const periods = utils.getStatsPeriods(now);
  94. for await (const visit of visitsStream) {
  95. periods.forEach(([type, fromDate]) => {
  96. const isIncluded = isAfter(utils.parseDatetime(visit.created_at), fromDate);
  97. if (!isIncluded) return;
  98. const diffFunction = utils.getDifferenceFunction(type);
  99. const diff = diffFunction(now, utils.parseDatetime(visit.created_at));
  100. const index = stats[type].views.length - diff - 1;
  101. const view = stats[type].views[index];
  102. const period = stats[type].stats;
  103. const countries = typeof visit.countries === "string" ? JSON.parse(visit.countries) : visit.countries;
  104. const referrers = typeof visit.referrers === "string" ? JSON.parse(visit.referrers) : visit.referrers;
  105. stats[type].stats = {
  106. browser: {
  107. chrome: period.browser.chrome + visit.br_chrome,
  108. edge: period.browser.edge + visit.br_edge,
  109. firefox: period.browser.firefox + visit.br_firefox,
  110. ie: period.browser.ie + visit.br_ie,
  111. opera: period.browser.opera + visit.br_opera,
  112. other: period.browser.other + visit.br_other,
  113. safari: period.browser.safari + visit.br_safari
  114. },
  115. os: {
  116. android: period.os.android + visit.os_android,
  117. ios: period.os.ios + visit.os_ios,
  118. linux: period.os.linux + visit.os_linux,
  119. macos: period.os.macos + visit.os_macos,
  120. other: period.os.other + visit.os_other,
  121. windows: period.os.windows + visit.os_windows
  122. },
  123. country: {
  124. ...period.country,
  125. ...Object.entries(countries).reduce(
  126. (obj, [country, count]) => ({
  127. ...obj,
  128. [country]: (period.country[country] || 0) + count
  129. }),
  130. {}
  131. )
  132. },
  133. referrer: {
  134. ...period.referrer,
  135. ...Object.entries(referrers).reduce(
  136. (obj, [referrer, count]) => ({
  137. ...obj,
  138. [referrer]: (period.referrer[referrer] || 0) + count
  139. }),
  140. {}
  141. )
  142. }
  143. };
  144. stats[type].views[index] += visit.total;
  145. stats[type].total += visit.total;
  146. });
  147. }
  148. const response = {
  149. lastYear: {
  150. stats: utils.statsObjectToArray(stats.lastYear.stats),
  151. views: stats.lastYear.views,
  152. total: stats.lastYear.total
  153. },
  154. lastDay: {
  155. stats: utils.statsObjectToArray(stats.lastDay.stats),
  156. views: stats.lastDay.views,
  157. total: stats.lastDay.total
  158. },
  159. lastMonth: {
  160. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  161. views: stats.lastMonth.views,
  162. total: stats.lastMonth.total
  163. },
  164. lastWeek: {
  165. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  166. views: stats.lastWeek.views,
  167. total: stats.lastWeek.total
  168. },
  169. updatedAt: new Date()
  170. };
  171. if (match.link_id && env.REDIS_ENABLED) {
  172. const key = redis.key.stats(match.link_id);
  173. redis.client.set(key, JSON.stringify(response), "EX", 60);
  174. }
  175. return response;
  176. };
  177. module.exports = {
  178. add,
  179. find
  180. };