visit.queries.js 5.4 KB

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